From d6ba3d64a728124b64760735183d6788fd2a7f6b Mon Sep 17 00:00:00 2001 From: Radmir <52320354+DRadmir@users.noreply.github.com> Date: Fri, 5 Dec 2025 17:21:55 +0500 Subject: [PATCH 01/11] Add TON WalletConnect support --- crates/primitives/src/lib.rs | 2 +- crates/primitives/src/wallet_connect.rs | 10 ++ .../src/wallet_connect_namespace.rs | 21 +++- crates/primitives/src/wallet_connector.rs | 4 + gemstone/src/config/wallet_connect.rs | 2 +- gemstone/src/message/decoder.rs | 8 +- gemstone/src/message/sign_type.rs | 1 + gemstone/src/wallet_connect/actions.rs | 11 ++ gemstone/src/wallet_connect/mod.rs | 75 ++++++++++++- .../src/wallet_connect/request_handler/mod.rs | 4 + .../src/wallet_connect/request_handler/ton.rs | 100 ++++++++++++++++++ .../src/wallet_connect/response_handler.rs | 35 +++++- 12 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 gemstone/src/wallet_connect/request_handler/ton.rs diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index df4fceacd..9d553b815 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -109,7 +109,7 @@ pub use self::transaction_metadata_types::{TransactionNFTTransferMetadata, Trans pub mod wallet_connect_namespace; pub use self::wallet_connect_namespace::WalletConnectCAIP2; pub mod wallet_connect; -pub use self::wallet_connect::{WCEthereumTransaction, WalletConnectRequest}; +pub use self::wallet_connect::{WCEthereumTransaction, WCTonMessage, WalletConnectRequest}; pub mod account; pub use self::account::Account; pub mod wallet; diff --git a/crates/primitives/src/wallet_connect.rs b/crates/primitives/src/wallet_connect.rs index eea033fa6..954f9644c 100644 --- a/crates/primitives/src/wallet_connect.rs +++ b/crates/primitives/src/wallet_connect.rs @@ -18,6 +18,16 @@ pub struct WCEthereumTransaction { pub data: Option, } +#[derive(Debug, Serialize, Deserialize)] +#[typeshare(swift = "Equatable, Hashable, Sendable")] +#[serde(rename_all = "camelCase")] +pub struct WCTonMessage { + pub address: String, + pub amount: String, + pub payload: Option, + pub state_init: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WalletConnectRequest { diff --git a/crates/primitives/src/wallet_connect_namespace.rs b/crates/primitives/src/wallet_connect_namespace.rs index 727875fdc..06c96ef0a 100644 --- a/crates/primitives/src/wallet_connect_namespace.rs +++ b/crates/primitives/src/wallet_connect_namespace.rs @@ -17,6 +17,8 @@ pub enum WalletConnectCAIP2 { Algorand, #[serde(rename = "sui")] Sui, + #[serde(rename = "ton")] + Ton, } impl WalletConnectCAIP2 { @@ -27,8 +29,8 @@ impl WalletConnectCAIP2 { ChainType::Cosmos => Some(format!("{}:{}", WalletConnectCAIP2::Cosmos.as_ref(), chain.network_id())), ChainType::Algorand => Some(WalletConnectCAIP2::Algorand.as_ref().to_string()), ChainType::Sui => Some(WalletConnectCAIP2::Sui.as_ref().to_string()), + ChainType::Ton => Some(WalletConnectCAIP2::Ton.as_ref().to_string()), ChainType::Bitcoin - | ChainType::Ton | ChainType::Tron | ChainType::Aptos | ChainType::Xrp @@ -47,6 +49,7 @@ impl WalletConnectCAIP2 { WalletConnectCAIP2::Cosmos => Some(ChainType::Cosmos), WalletConnectCAIP2::Algorand => Some(ChainType::Algorand), WalletConnectCAIP2::Sui => Some(ChainType::Sui), + WalletConnectCAIP2::Ton => Some(ChainType::Ton), } } @@ -65,6 +68,7 @@ impl WalletConnectCAIP2 { WalletConnectCAIP2::Solana => Some(Chain::Solana), WalletConnectCAIP2::Algorand => Some(Chain::Algorand), WalletConnectCAIP2::Sui => Some(Chain::Sui), + WalletConnectCAIP2::Ton => Some(Chain::Ton), } } @@ -75,8 +79,8 @@ impl WalletConnectCAIP2 { ChainType::Cosmos => Self::get_namespace(chain).map(|namespace| format!("{}:{}", namespace, chain.network_id())), ChainType::Algorand => Some("wGHE2Pwdvd7S12BL5FaOP20EGYesN73k".to_string()), ChainType::Sui => Some("mainnet".to_string()), + ChainType::Ton => Some("-239".to_string()), ChainType::Bitcoin - | ChainType::Ton | ChainType::Tron | ChainType::Aptos | ChainType::Xrp @@ -114,6 +118,7 @@ mod tests { assert_eq!(WalletConnectCAIP2::get_chain_type("cosmos".to_string()), Some(ChainType::Cosmos)); assert_eq!(WalletConnectCAIP2::get_chain_type("algorand".to_string()), Some(ChainType::Algorand)); assert_eq!(WalletConnectCAIP2::get_chain_type("sui".to_string()), Some(ChainType::Sui)); + assert_eq!(WalletConnectCAIP2::get_chain_type("ton".to_string()), Some(ChainType::Ton)); assert_eq!(WalletConnectCAIP2::get_chain_type("unknown".to_string()), None); } @@ -123,6 +128,7 @@ mod tests { assert_eq!(WalletConnectCAIP2::get_chain("eip155".to_string(), "56".to_string()), Some(Chain::SmartChain)); assert_eq!(WalletConnectCAIP2::get_chain("solana".to_string(), "ignored".to_string()), Some(Chain::Solana)); assert_eq!(WalletConnectCAIP2::get_chain("sui".to_string(), "mainnet".to_string()), Some(Chain::Sui)); + assert_eq!(WalletConnectCAIP2::get_chain("ton".to_string(), "-239".to_string()), Some(Chain::Ton)); } #[test] @@ -133,9 +139,20 @@ mod tests { Ok(Chain::Solana) ); assert_eq!(WalletConnectCAIP2::resolve_chain(Some("sui:mainnet".to_string())), Ok(Chain::Sui)); + assert_eq!(WalletConnectCAIP2::resolve_chain(Some("ton:-239".to_string())), Ok(Chain::Ton)); assert!(WalletConnectCAIP2::resolve_chain(Some("invalid".to_string())).is_err()); assert!(WalletConnectCAIP2::resolve_chain(Some("eip155:1:extra".to_string())).is_err()); assert!(WalletConnectCAIP2::resolve_chain(None).is_err()); assert!(WalletConnectCAIP2::resolve_chain(Some("unknown:chain".to_string())).is_err()); } + + #[test] + fn test_get_namespace_ton() { + assert_eq!(WalletConnectCAIP2::get_namespace(Chain::Ton), Some("ton".to_string())); + } + + #[test] + fn test_get_reference_ton() { + assert_eq!(WalletConnectCAIP2::get_reference(Chain::Ton), Some("-239".to_string())); + } } diff --git a/crates/primitives/src/wallet_connector.rs b/crates/primitives/src/wallet_connector.rs index 9efe98dcd..163a9166b 100644 --- a/crates/primitives/src/wallet_connector.rs +++ b/crates/primitives/src/wallet_connector.rs @@ -53,6 +53,10 @@ pub enum WalletConnectionMethods { SuiSignTransaction, #[serde(rename = "sui_signAndExecuteTransaction")] SuiSignAndExecuteTransaction, + #[serde(rename = "ton_sendMessage")] + TonSendMessage, + #[serde(rename = "ton_signData")] + TonSignData, } #[derive(Debug, Serialize)] diff --git a/gemstone/src/config/wallet_connect.rs b/gemstone/src/config/wallet_connect.rs index dcbc0322b..674d0525f 100644 --- a/gemstone/src/config/wallet_connect.rs +++ b/gemstone/src/config/wallet_connect.rs @@ -6,7 +6,7 @@ pub struct WalletConnectConfig { } pub fn get_wallet_connect_config() -> WalletConnectConfig { - let chains: Vec = [vec![Chain::Solana, Chain::Sui], EVMChain::all().iter().map(|x| x.to_chain()).collect()].concat(); + let chains: Vec = [vec![Chain::Solana, Chain::Sui, Chain::Ton], EVMChain::all().iter().map(|x| x.to_chain()).collect()].concat(); WalletConnectConfig { chains: chains.into_iter().map(|x| x.to_string()).collect(), diff --git a/gemstone/src/message/decoder.rs b/gemstone/src/message/decoder.rs index 1b4f9b3df..288cec8ad 100644 --- a/gemstone/src/message/decoder.rs +++ b/gemstone/src/message/decoder.rs @@ -34,7 +34,7 @@ impl SignMessageDecoder { pub fn preview(&self) -> Result { match self.message.sign_type { - SignDigestType::SuiPersonal | SignDigestType::Eip191 => { + SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::TonPersonal => { let string = String::from_utf8(self.message.data.clone()); let preview = string.unwrap_or(encode_prefixed(&self.message.data)); Ok(MessagePreview::Text(preview)) @@ -70,7 +70,7 @@ impl SignMessageDecoder { pub fn plain_preview(&self) -> String { match self.message.sign_type { - SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::Base58 => match self.preview() { + SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::TonPersonal => match self.preview() { Ok(MessagePreview::Text(preview)) => preview, _ => "".to_string(), }, @@ -84,7 +84,7 @@ impl SignMessageDecoder { pub fn hash(&self) -> Vec { match &self.message.sign_type { - SignDigestType::SuiPersonal => self.message.data.clone(), + SignDigestType::SuiPersonal | SignDigestType::TonPersonal => self.message.data.clone(), SignDigestType::Eip191 | SignDigestType::Siwe => eip191_hash_message(&self.message.data).to_vec(), SignDigestType::Eip712 => match std::str::from_utf8(&self.message.data) { Ok(json) => hash_eip712(json).map(|digest| digest.to_vec()).unwrap_or_default(), @@ -111,7 +111,7 @@ impl SignMessageDecoder { } encode_prefixed(&signature) } - SignDigestType::SuiPersonal => BASE64.encode(data), + SignDigestType::SuiPersonal | SignDigestType::TonPersonal => BASE64.encode(data), SignDigestType::Base58 => bs58::encode(data).into_string(), } } diff --git a/gemstone/src/message/sign_type.rs b/gemstone/src/message/sign_type.rs index 8c9319657..812dc88d4 100644 --- a/gemstone/src/message/sign_type.rs +++ b/gemstone/src/message/sign_type.rs @@ -7,6 +7,7 @@ pub enum SignDigestType { Base58, SuiPersonal, Siwe, + TonPersonal, } #[derive(Debug, uniffi::Record)] diff --git a/gemstone/src/wallet_connect/actions.rs b/gemstone/src/wallet_connect/actions.rs index 2eada7ab6..e9545f54c 100644 --- a/gemstone/src/wallet_connect/actions.rs +++ b/gemstone/src/wallet_connect/actions.rs @@ -27,6 +27,12 @@ pub struct WCSuiTransactionData { pub wallet_address: String, } +#[derive(Debug, Clone, uniffi::Record)] +pub struct WCTonTransactionData { + pub messages: String, + pub valid_until: Option, +} + #[derive(Debug, Clone, uniffi::Enum)] pub enum WalletConnectAction { SignMessage { @@ -57,6 +63,7 @@ pub enum WalletConnectTransactionType { Ethereum, Solana { output_type: TransferDataOutputType }, Sui { output_type: TransferDataOutputType }, + Ton { output_type: TransferDataOutputType }, } #[derive(Debug, Clone, uniffi::Enum)] @@ -73,6 +80,10 @@ pub enum WalletConnectTransaction { data: WCSuiTransactionData, output_type: TransferDataOutputType, }, + Ton { + data: WCTonTransactionData, + output_type: TransferDataOutputType, + }, } #[derive(Debug, Clone, uniffi::Enum)] diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index 895ca8420..eb3c7205e 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -2,6 +2,7 @@ use crate::{ message::sign_type::{SignDigestType, SignMessage}, siwe::SiweMessage, }; +use base64::Engine as _; use hex::FromHex; use primitives::{Chain, WCEthereumTransaction, WalletConnectRequest, WalletConnectionVerificationStatus}; use std::str::FromStr; @@ -81,6 +82,30 @@ mod tests { assert!(matches!(decoded.sign_type, SignDigestType::Eip191)); } + + #[test] + fn decode_ton_sign_message_text() { + let wallet_connect = WalletConnect::new(); + let data = r#"{"type":"text","text":"Hello TON"}"#.to_string(); + + let decoded = wallet_connect.decode_sign_message(Chain::Ton, SignDigestType::TonPersonal, data); + + assert!(matches!(decoded.sign_type, SignDigestType::TonPersonal)); + assert_eq!(decoded.chain, Chain::Ton); + assert_eq!(decoded.data, b"Hello TON".to_vec()); + } + + #[test] + fn decode_ton_sign_message_binary() { + let wallet_connect = WalletConnect::new(); + let data = r#"{"type":"binary","data":"SGVsbG8gVE9O"}"#.to_string(); + + let decoded = wallet_connect.decode_sign_message(Chain::Ton, SignDigestType::TonPersonal, data); + + assert!(matches!(decoded.sign_type, SignDigestType::TonPersonal)); + assert_eq!(decoded.chain, Chain::Ton); + assert_eq!(decoded.data, b"Hello TON".to_vec()); + } } #[uniffi::export] @@ -143,11 +168,15 @@ impl WalletConnect { })?; gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } - SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe => Ok(()), + SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe | SignDigestType::TonPersonal => Ok(()), } } pub fn decode_sign_message(&self, chain: Chain, sign_type: SignDigestType, data: String) -> SignMessage { + if matches!(sign_type, SignDigestType::TonPersonal) { + return self.decode_ton_sign_message(data); + } + let mut utf8_value = None; let message_data = if let Some(stripped) = data.strip_prefix("0x") { Vec::from_hex(stripped).unwrap_or_else(|_| data.as_bytes().to_vec()) @@ -169,6 +198,33 @@ impl WalletConnect { } } + fn decode_ton_sign_message(&self, data: String) -> SignMessage { + if let Ok(json) = serde_json::from_str::(&data) { + let payload_type = json.get("type").and_then(|v| v.as_str()).unwrap_or("text"); + + let message_data = match payload_type { + "text" => json.get("text").and_then(|v| v.as_str()).unwrap_or_default().as_bytes().to_vec(), + "binary" => { + let binary_data = json.get("data").and_then(|v| v.as_str()).unwrap_or_default(); + base64::engine::general_purpose::STANDARD.decode(binary_data).unwrap_or_default() + } + _ => data.as_bytes().to_vec(), + }; + + return SignMessage { + chain: Chain::Ton, + sign_type: SignDigestType::TonPersonal, + data: message_data, + }; + } + + SignMessage { + chain: Chain::Ton, + sign_type: SignDigestType::TonPersonal, + data: data.as_bytes().to_vec(), + } + } + fn decode_siwe_message(&self, chain: Chain, raw_text: &str, message_data: &[u8]) -> Option { let message = SiweMessage::try_parse(raw_text)?; message.validate(chain).ok()?; @@ -229,6 +285,23 @@ impl WalletConnect { output_type, }) } + WalletConnectTransactionType::Ton { output_type } => { + let json: serde_json::Value = serde_json::from_str(&data)?; + + let messages = json + .get("messages") + .ok_or_else(|| crate::GemstoneError::AnyError { + msg: "Missing messages field".to_string(), + })? + .to_string(); + + let valid_until = json.get("valid_until").and_then(|v| v.as_i64()); + + Ok(WalletConnectTransaction::Ton { + data: actions::WCTonTransactionData { messages, valid_until }, + output_type, + }) + } } } } diff --git a/gemstone/src/wallet_connect/request_handler/mod.rs b/gemstone/src/wallet_connect/request_handler/mod.rs index 7da4059e8..e8de90b71 100644 --- a/gemstone/src/wallet_connect/request_handler/mod.rs +++ b/gemstone/src/wallet_connect/request_handler/mod.rs @@ -1,6 +1,7 @@ mod ethereum; mod solana; mod sui; +mod ton; use crate::wallet_connect::actions::{WalletConnectAction, WalletConnectChainOperation}; use crate::wallet_connect::handler_traits::ChainRequestHandler; @@ -9,6 +10,7 @@ use primitives::{Chain, WalletConnectRequest, WalletConnectionMethods}; use serde_json::Value; use solana::SolanaRequestHandler; use sui::SuiRequestHandler; +use ton::TonRequestHandler; pub struct WalletConnectRequestHandler; @@ -52,6 +54,8 @@ impl WalletConnectRequestHandler { WalletConnectionMethods::SuiSignPersonalMessage => SuiRequestHandler::parse_sign_message(Chain::Sui, params), WalletConnectionMethods::SuiSignTransaction => SuiRequestHandler::parse_sign_transaction(Chain::Sui, params), WalletConnectionMethods::SuiSignAndExecuteTransaction => SuiRequestHandler::parse_send_transaction(Chain::Sui, params), + WalletConnectionMethods::TonSignData => TonRequestHandler::parse_sign_message(Chain::Ton, params), + WalletConnectionMethods::TonSendMessage => TonRequestHandler::parse_send_transaction(Chain::Ton, params), } } diff --git a/gemstone/src/wallet_connect/request_handler/ton.rs b/gemstone/src/wallet_connect/request_handler/ton.rs new file mode 100644 index 000000000..6d1c9151e --- /dev/null +++ b/gemstone/src/wallet_connect/request_handler/ton.rs @@ -0,0 +1,100 @@ +use crate::message::sign_type::SignDigestType; +use crate::wallet_connect::actions::{WalletConnectAction, WalletConnectTransactionType}; +use crate::wallet_connect::handler_traits::ChainRequestHandler; +use primitives::{Chain, TransferDataOutputType}; +use serde_json::Value; + +pub struct TonRequestHandler; + +impl ChainRequestHandler for TonRequestHandler { + fn parse_sign_message(_chain: Chain, params: Value) -> Result { + let params_array = params.as_array().ok_or("Invalid params format")?; + let payload = params_array.first().ok_or("Missing payload parameter")?; + let data = payload.to_string(); + Ok(WalletConnectAction::SignMessage { + chain: Chain::Ton, + sign_type: SignDigestType::TonPersonal, + data, + }) + } + + fn parse_sign_transaction(_chain: Chain, params: Value) -> Result { + params.get("messages").ok_or("Missing messages parameter")?; + Ok(WalletConnectAction::SignTransaction { + chain: Chain::Ton, + transaction_type: WalletConnectTransactionType::Ton { + output_type: TransferDataOutputType::Signature, + }, + data: params.to_string(), + }) + } + + fn parse_send_transaction(_chain: Chain, params: Value) -> Result { + params.get("messages").ok_or("Missing messages parameter")?; + Ok(WalletConnectAction::SendTransaction { + chain: Chain::Ton, + transaction_type: WalletConnectTransactionType::Ton { + output_type: TransferDataOutputType::EncodedTransaction, + }, + data: params.to_string(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_sign_message() { + let params = serde_json::from_str(r#"[{"type":"text","text":"Hello TON"}]"#).unwrap(); + let action = TonRequestHandler::parse_sign_message(Chain::Ton, params).unwrap(); + match action { + WalletConnectAction::SignMessage { chain, sign_type, data } => { + assert_eq!(chain, Chain::Ton); + assert!(matches!(sign_type, SignDigestType::TonPersonal)); + assert!(data.contains("Hello TON")); + } + _ => panic!("Expected SignMessage action"), + } + } + + #[test] + fn test_parse_send_transaction() { + let params_json = r#"{ + "valid_until": 1234567890, + "messages": [ + { + "address": "0:1234567890abcdef", + "amount": "1000000000" + } + ] + }"#; + let params = serde_json::from_str(params_json).unwrap(); + let action = TonRequestHandler::parse_send_transaction(Chain::Ton, params).unwrap(); + + match action { + WalletConnectAction::SendTransaction { chain, transaction_type, data } => { + assert_eq!(chain, Chain::Ton); + assert!(matches!( + transaction_type, + WalletConnectTransactionType::Ton { + output_type: TransferDataOutputType::EncodedTransaction + } + )); + let parsed_data: serde_json::Value = serde_json::from_str(&data).expect("Data should be valid JSON"); + assert!(parsed_data.get("messages").is_some()); + assert_eq!(parsed_data.get("valid_until").and_then(|v| v.as_i64()), Some(1234567890)); + } + _ => panic!("Expected SendTransaction action"), + } + } + + #[test] + fn test_parse_send_transaction_missing_messages() { + let params = serde_json::from_str(r#"{"valid_until": 123}"#).unwrap(); + let result = TonRequestHandler::parse_send_transaction(Chain::Ton, params); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), "Missing messages parameter"); + } +} diff --git a/gemstone/src/wallet_connect/response_handler.rs b/gemstone/src/wallet_connect/response_handler.rs index 671d1841e..7585266e4 100644 --- a/gemstone/src/wallet_connect/response_handler.rs +++ b/gemstone/src/wallet_connect/response_handler.rs @@ -34,13 +34,14 @@ impl WalletConnectResponseHandler { json: serde_json::to_string(&result).unwrap_or_default(), } } + ChainType::Ton => WalletConnectResponseType::Object { json: signature }, _ => WalletConnectResponseType::String { value: signature }, } } pub fn encode_sign_transaction(chain_type: ChainType, transaction_id: String) -> WalletConnectResponseType { match chain_type { - ChainType::Solana => WalletConnectResponseType::Object { + ChainType::Solana | ChainType::Ton => WalletConnectResponseType::Object { json: serde_json::json!({ "signature": transaction_id }).to_string(), }, ChainType::Sui => { @@ -155,4 +156,36 @@ mod tests { _ => panic!("Expected Object response for Sui"), } } + + #[test] + fn test_encode_sign_message_ton() { + let ton_json = r#"{"signature":"tonsig123","timestamp":1700000000}"#.to_string(); + let result = WalletConnectResponseHandler::encode_sign_message(ChainType::Ton, ton_json); + match result { + WalletConnectResponseType::Object { json } => { + assert!(json.contains("\"signature\"")); + assert!(json.contains("tonsig123")); + assert!(json.contains("\"timestamp\"")); + } + _ => panic!("Expected Object response for Ton"), + } + } + + #[test] + fn test_encode_sign_transaction_ton() { + let result = WalletConnectResponseHandler::encode_sign_transaction(ChainType::Ton, "tontxsig".to_string()); + match result { + WalletConnectResponseType::Object { json } => { + assert!(json.contains("\"signature\"")); + assert!(json.contains("tontxsig")); + } + _ => panic!("Expected Object response for Ton"), + } + } + + #[test] + fn test_encode_send_transaction_ton() { + let result = WalletConnectResponseHandler::encode_send_transaction(ChainType::Ton, "tonhash123".to_string()); + assert!(matches!(result, WalletConnectResponseType::String { value } if value == "tonhash123")); + } } From 1cb77e19fe43501310a37bdd44ae6fda2c8ee0c2 Mon Sep 17 00:00:00 2001 From: Radmir <52320354+DRadmir@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:40:24 +0500 Subject: [PATCH 02/11] Add TON WalletConnect signData and sendTransaction support --- crates/gem_ton/src/address.rs | 8 + crates/signer/src/lib.rs | 22 ++- crates/signer/src/ton.rs | 163 ++++++++++++++++++ gemstone/src/message/decoder.rs | 35 +++- gemstone/src/signer/crypto.rs | 13 +- gemstone/src/wallet_connect/actions.rs | 8 +- gemstone/src/wallet_connect/mod.rs | 124 ++++++------- .../src/wallet_connect/request_handler/ton.rs | 38 ++-- 8 files changed, 311 insertions(+), 100 deletions(-) create mode 100644 crates/signer/src/ton.rs diff --git a/crates/gem_ton/src/address.rs b/crates/gem_ton/src/address.rs index 0cfcef4da..bbc5fa4c1 100644 --- a/crates/gem_ton/src/address.rs +++ b/crates/gem_ton/src/address.rs @@ -172,4 +172,12 @@ mod tests { assert_eq!(original_hex, decoded_hex); } + + #[test] + fn test_base64_to_hex_user_address() { + let base64 = "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"; + let hex = base64_to_hex_address(base64.to_string()).unwrap(); + + assert!(hex.starts_with("0:") || hex.starts_with("-1:")); + } } diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index db80dcbea..d7a7f2beb 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -3,19 +3,23 @@ mod eip712; mod error; mod secp256k1; mod sui; +mod ton; -pub use eip712::hash_typed_data as hash_eip712; -pub use error::SignerError; -pub use sui::SUI_PERSONAL_MESSAGE_SIGNATURE_LEN; +use std::borrow::Cow; -use crate::ed25519::{sign_digest as sign_ed25519_digest, signing_key_from_bytes}; -use crate::secp256k1::sign_digest as sign_secp256k1_digest; -use crate::sui::assemble_signature; use ed25519_dalek::Signer as DalekSigner; -use std::borrow::Cow; use sui_types::PersonalMessage; use zeroize::Zeroizing; +use crate::ed25519::{sign_digest as sign_ed25519_digest, signing_key_from_bytes}; +use crate::secp256k1::sign_digest as sign_secp256k1_digest; +use crate::sui::assemble_signature; + +pub use eip712::hash_typed_data as hash_eip712; +pub use error::SignerError; +pub use sui::SUI_PERSONAL_MESSAGE_SIGNATURE_LEN; +pub use ton::TonSignDataInput; + #[derive(Debug, Default)] pub struct Signer; @@ -57,6 +61,10 @@ impl Signer { let signature = Self::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key_vec.to_vec())?; Ok(hex::encode(signature)) } + + pub fn sign_ton_personal_message(input: TonSignDataInput, private_key: Vec) -> Result { + ton::sign_ton_personal_message(input, private_key) + } } #[cfg(test)] diff --git a/crates/signer/src/ton.rs b/crates/signer/src/ton.rs new file mode 100644 index 000000000..7c5a8a41e --- /dev/null +++ b/crates/signer/src/ton.rs @@ -0,0 +1,163 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use base64::Engine; +use base64::engine::general_purpose::STANDARD; +use ed25519_dalek::Signer as DalekSigner; +use serde::{Deserialize, Serialize}; +use zeroize::Zeroizing; + +use crate::ed25519::signing_key_from_bytes; +use crate::error::SignerError; + +#[derive(Clone, Debug, PartialEq)] +enum TonSignDataType { + Text, + Binary, + Cell, +} + +impl TonSignDataType { + fn as_str(&self) -> &'static str { + match self { + TonSignDataType::Text => "text", + TonSignDataType::Binary => "binary", + TonSignDataType::Cell => "cell", + } + } + + fn data_field(&self) -> &'static str { + match self { + TonSignDataType::Text => "text", + TonSignDataType::Binary => "bytes", + TonSignDataType::Cell => "cell", + } + } +} + +#[derive(Deserialize)] +struct TonSignDataPayloadRaw { + #[serde(rename = "type")] + payload_type: String, + text: Option, + bytes: Option, + cell: Option, +} + +struct TonSignDataPayload { + payload_type: TonSignDataType, + data: String, +} + +impl TonSignDataPayload { + fn parse(json: &str) -> Result { + let raw: TonSignDataPayloadRaw = serde_json::from_str(json)?; + + let (payload_type, data) = match raw.payload_type.as_str() { + "text" => (TonSignDataType::Text, raw.text.ok_or("Missing text field")?), + "binary" => (TonSignDataType::Binary, raw.bytes.ok_or("Missing bytes field")?), + "cell" => (TonSignDataType::Cell, raw.cell.ok_or("Missing cell field")?), + _ => return Err(SignerError::new(format!("Unknown payload type: {}", raw.payload_type))), + }; + + Ok(Self { payload_type, data }) + } +} + +pub struct TonSignDataInput { + pub domain: String, + pub payload: Vec, +} + +#[derive(Serialize)] +struct TonSignDataResponse { + signature: String, + #[serde(rename = "publicKey")] + public_key: String, + timestamp: u64, + domain: String, + payload: serde_json::Value, +} + +pub fn sign_ton_personal_message(input: TonSignDataInput, private_key: Vec) -> Result { + let private_key = Zeroizing::new(private_key); + let payload_str = std::str::from_utf8(&input.payload).map_err(|e| SignerError::new(e.to_string()))?; + let parsed = TonSignDataPayload::parse(payload_str)?; + + let signing_key = signing_key_from_bytes(&private_key)?; + let signature = signing_key.sign(parsed.data.as_bytes()); + + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); + + let payload = serde_json::json!({ + "type": parsed.payload_type.as_str(), + parsed.payload_type.data_field(): parsed.data, + }); + + let response = TonSignDataResponse { + signature: STANDARD.encode(signature.to_bytes()), + public_key: STANDARD.encode(signing_key.verifying_key().to_bytes()), + timestamp, + domain: input.domain, + payload, + }; + + Ok(serde_json::to_string(&response)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::Verifier; + + const TEST_PRIVATE_KEY: &str = "1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34"; + + #[test] + fn test_parse_payload_text() { + let json = r#"{"type":"text","text":"Hello TON"}"#; + let parsed = TonSignDataPayload::parse(json).unwrap(); + + assert_eq!(parsed.payload_type, TonSignDataType::Text); + assert_eq!(parsed.data, "Hello TON"); + } + + #[test] + fn test_verify_signature() { + let private_key = hex::decode(TEST_PRIVATE_KEY).unwrap(); + let text = "Hello TON"; + let input = TonSignDataInput { + domain: "example.com".to_string(), + payload: format!(r#"{{"type":"text","text":"{}"}}"#, text).into_bytes(), + }; + + let result_json = sign_ton_personal_message(input, private_key.clone()).unwrap(); + let result: serde_json::Value = serde_json::from_str(&result_json).unwrap(); + + let signing_key = signing_key_from_bytes(&private_key).unwrap(); + let public_key = signing_key.verifying_key(); + + let signature_base64 = result["signature"].as_str().unwrap(); + let signature_bytes = STANDARD.decode(signature_base64).unwrap(); + let signature = ed25519_dalek::Signature::from_slice(&signature_bytes).unwrap(); + + assert!(public_key.verify(text.as_bytes(), &signature).is_ok()); + assert_eq!(result["publicKey"].as_str().unwrap(), STANDARD.encode(public_key.to_bytes())); + assert!(result["timestamp"].as_u64().unwrap() > 0); + } + + #[test] + fn test_response_format_text() { + let private_key = hex::decode(TEST_PRIVATE_KEY).unwrap(); + let input = TonSignDataInput { + domain: "react-app.walletconnect.com".to_string(), + payload: r#"{"type":"text","text":"Hello from WalletConnect TON"}"#.as_bytes().to_vec(), + }; + + let result_json = sign_ton_personal_message(input, private_key).unwrap(); + let result: serde_json::Value = serde_json::from_str(&result_json).unwrap(); + + assert_eq!(result["domain"], "react-app.walletconnect.com"); + assert!(result["timestamp"].as_u64().unwrap() > 0); + assert_eq!(result["payload"]["type"], "text"); + assert_eq!(result["payload"]["text"], "Hello from WalletConnect TON"); + } +} diff --git a/gemstone/src/message/decoder.rs b/gemstone/src/message/decoder.rs index 288cec8ad..699e7b08c 100644 --- a/gemstone/src/message/decoder.rs +++ b/gemstone/src/message/decoder.rs @@ -34,11 +34,27 @@ impl SignMessageDecoder { pub fn preview(&self) -> Result { match self.message.sign_type { - SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::TonPersonal => { + SignDigestType::SuiPersonal | SignDigestType::Eip191 => { let string = String::from_utf8(self.message.data.clone()); let preview = string.unwrap_or(encode_prefixed(&self.message.data)); Ok(MessagePreview::Text(preview)) } + SignDigestType::TonPersonal => { + let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8"))?; + let Some(json) = serde_json::from_str::(&string).ok() else { + return Ok(MessagePreview::Text(string)); + }; + let Some(payload_type) = json.get("type").and_then(|v| v.as_str()) else { + return Ok(MessagePreview::Text(string)); + }; + let preview = match payload_type { + "text" => json.get("text").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + "binary" => json.get("bytes").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + "cell" => json.get("cell").and_then(|v| v.as_str()).unwrap_or_default().to_string(), + _ => string, + }; + Ok(MessagePreview::Text(preview)) + } SignDigestType::Eip712 => { let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8 string for EIP712"))?; if string.is_empty() { @@ -428,4 +444,21 @@ mod tests { assert_eq!(decoder.plain_preview(), message); } + + #[test] + fn test_ton_personal_preview() { + let data = r#"{"type":"text","text":"Hello TON","from":"UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}"#; + let decoder = SignMessageDecoder::new(SignMessage { + chain: Chain::Ton, + sign_type: SignDigestType::TonPersonal, + data: data.as_bytes().to_vec(), + }); + + match decoder.preview() { + Ok(MessagePreview::Text(preview)) => assert_eq!(preview, "Hello TON"), + other => panic!("Unexpected result: {other:?}"), + } + + assert_eq!(decoder.plain_preview(), "Hello TON"); + } } diff --git a/gemstone/src/signer/crypto.rs b/gemstone/src/signer/crypto.rs index 40c589295..13d9f2012 100644 --- a/gemstone/src/signer/crypto.rs +++ b/gemstone/src/signer/crypto.rs @@ -1,5 +1,7 @@ +use gem_ton::address::base64_to_hex_address; +use signer::{SignatureScheme as GemSignatureScheme, Signer, TonSignDataInput as SignerTonSignDataInput}; + use crate::GemstoneError; -use ::signer::{SignatureScheme as GemSignatureScheme, Signer}; #[derive(Default, uniffi::Object)] pub struct CryptoSigner; @@ -22,6 +24,15 @@ impl CryptoSigner { pub fn sign_digest(&self, scheme: GemSignatureScheme, digest: Vec, private_key: Vec) -> Result, GemstoneError> { Signer::sign_digest(scheme, digest, private_key).map_err(GemstoneError::from) } + + pub fn sign_ton_personal_message(&self, domain: String, payload: Vec, private_key: Vec) -> Result { + let signer_input = SignerTonSignDataInput { domain, payload }; + Signer::sign_ton_personal_message(signer_input, private_key).map_err(GemstoneError::from) + } + + pub fn ton_base64_to_raw_address(&self, base64_address: String) -> Result { + base64_to_hex_address(base64_address).map_err(|e| GemstoneError::AnyError { msg: e.to_string() }) + } } #[uniffi::remote(Enum)] diff --git a/gemstone/src/wallet_connect/actions.rs b/gemstone/src/wallet_connect/actions.rs index e9545f54c..c60463600 100644 --- a/gemstone/src/wallet_connect/actions.rs +++ b/gemstone/src/wallet_connect/actions.rs @@ -27,12 +27,6 @@ pub struct WCSuiTransactionData { pub wallet_address: String, } -#[derive(Debug, Clone, uniffi::Record)] -pub struct WCTonTransactionData { - pub messages: String, - pub valid_until: Option, -} - #[derive(Debug, Clone, uniffi::Enum)] pub enum WalletConnectAction { SignMessage { @@ -81,7 +75,7 @@ pub enum WalletConnectTransaction { output_type: TransferDataOutputType, }, Ton { - data: WCTonTransactionData, + messages: String, output_type: TransferDataOutputType, }, } diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index eb3c7205e..2c09baae0 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -1,11 +1,15 @@ -use crate::{ - message::sign_type::{SignDigestType, SignMessage}, - siwe::SiweMessage, -}; -use base64::Engine as _; +use std::str::FromStr; +use std::time::{SystemTime, UNIX_EPOCH}; + use hex::FromHex; use primitives::{Chain, WCEthereumTransaction, WalletConnectRequest, WalletConnectionVerificationStatus}; -use std::str::FromStr; + +fn current_timestamp() -> i64 { + SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0) +} + +use crate::message::sign_type::{SignDigestType, SignMessage}; +use crate::siwe::SiweMessage; pub mod actions; pub mod handler_traits; @@ -58,7 +62,7 @@ mod tests { let decoded = wallet_connect.decode_sign_message(Chain::Ethereum, SignDigestType::Eip191, message.clone()); - assert!(matches!(decoded.sign_type, SignDigestType::Siwe)); + assert_eq!(decoded.sign_type, SignDigestType::Siwe); assert_eq!(decoded.data, message.into_bytes()); } @@ -69,7 +73,7 @@ mod tests { let decoded = wallet_connect.decode_sign_message(Chain::Ethereum, SignDigestType::Eip191, message.clone()); - assert!(matches!(decoded.sign_type, SignDigestType::Eip191)); + assert_eq!(decoded.sign_type, SignDigestType::Eip191); assert_eq!(decoded.data, message.into_bytes()); } @@ -80,31 +84,30 @@ mod tests { let decoded = wallet_connect.decode_sign_message(Chain::Polygon, SignDigestType::Eip191, message); - assert!(matches!(decoded.sign_type, SignDigestType::Eip191)); + assert_eq!(decoded.sign_type, SignDigestType::Eip191); } #[test] - fn decode_ton_sign_message_text() { + fn validate_ton_sign_message() { let wallet_connect = WalletConnect::new(); - let data = r#"{"type":"text","text":"Hello TON"}"#.to_string(); - - let decoded = wallet_connect.decode_sign_message(Chain::Ton, SignDigestType::TonPersonal, data); - assert!(matches!(decoded.sign_type, SignDigestType::TonPersonal)); - assert_eq!(decoded.chain, Chain::Ton); - assert_eq!(decoded.data, b"Hello TON".to_vec()); + assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"text":"Hello"}"#.to_string()).is_err()); + assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"unknown"}"#.to_string()).is_err()); + assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"text","text":"Hello"}"#.to_string()).is_ok()); + assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"binary","bytes":"SGVsbG8="}"#.to_string()).is_ok()); + assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"cell","cell":"te6c"}"#.to_string()).is_ok()); } #[test] - fn decode_ton_sign_message_binary() { + fn validate_ton_send_transaction() { let wallet_connect = WalletConnect::new(); - let data = r#"{"type":"binary","data":"SGVsbG8gVE9O"}"#.to_string(); - - let decoded = wallet_connect.decode_sign_message(Chain::Ton, SignDigestType::TonPersonal, data); + let ton_type = WalletConnectTransactionType::Ton { + output_type: primitives::TransferDataOutputType::EncodedTransaction, + }; - assert!(matches!(decoded.sign_type, SignDigestType::TonPersonal)); - assert_eq!(decoded.chain, Chain::Ton); - assert_eq!(decoded.data, b"Hello TON".to_vec()); + assert!(wallet_connect.validate_send_transaction(ton_type.clone(), r#"{"valid_until": 1234567890, "messages": []}"#.to_string()).is_err()); + assert!(wallet_connect.validate_send_transaction(ton_type.clone(), r#"{"valid_until": 9999999999, "messages": []}"#.to_string()).is_ok()); + assert!(wallet_connect.validate_send_transaction(ton_type, r#"{"messages": []}"#.to_string()).is_ok()); } } @@ -168,15 +171,44 @@ impl WalletConnect { })?; gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } - SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe | SignDigestType::TonPersonal => Ok(()), + SignDigestType::TonPersonal => { + let json: serde_json::Value = + serde_json::from_str(&data).map_err(|_| crate::GemstoneError::AnyError { msg: "Invalid JSON".to_string() })?; + let payload_type = json.get("type").and_then(|v| v.as_str()).ok_or_else(|| crate::GemstoneError::AnyError { + msg: "Missing type field".to_string(), + })?; + match payload_type { + "text" | "binary" | "cell" => Ok(()), + _ => Err(crate::GemstoneError::AnyError { + msg: format!("Unsupported payload type: {}", payload_type), + }), + } + } + SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe => Ok(()), } } - pub fn decode_sign_message(&self, chain: Chain, sign_type: SignDigestType, data: String) -> SignMessage { - if matches!(sign_type, SignDigestType::TonPersonal) { - return self.decode_ton_sign_message(data); + pub fn validate_send_transaction(&self, transaction_type: WalletConnectTransactionType, data: String) -> Result<(), crate::GemstoneError> { + let WalletConnectTransactionType::Ton { .. } = transaction_type else { + return Ok(()); + }; + + let json: serde_json::Value = serde_json::from_str(&data).map_err(|_| crate::GemstoneError::AnyError { + msg: "Invalid JSON".to_string(), + })?; + + if let Some(valid_until) = json.get("valid_until").and_then(|v| v.as_i64()) + && current_timestamp() >= valid_until + { + return Err(crate::GemstoneError::AnyError { + msg: "Transaction expired".to_string(), + }); } + Ok(()) + } + + pub fn decode_sign_message(&self, chain: Chain, sign_type: SignDigestType, data: String) -> SignMessage { let mut utf8_value = None; let message_data = if let Some(stripped) = data.strip_prefix("0x") { Vec::from_hex(stripped).unwrap_or_else(|_| data.as_bytes().to_vec()) @@ -187,7 +219,9 @@ impl WalletConnect { let raw_text = utf8_value.or_else(|| String::from_utf8(message_data.clone()).ok()).unwrap_or_default(); - if let Some(siwe_message) = self.decode_siwe_message(chain, &raw_text, &message_data) { + if sign_type == SignDigestType::Eip191 + && let Some(siwe_message) = self.decode_siwe_message(chain, &raw_text, &message_data) + { return siwe_message; } @@ -198,33 +232,6 @@ impl WalletConnect { } } - fn decode_ton_sign_message(&self, data: String) -> SignMessage { - if let Ok(json) = serde_json::from_str::(&data) { - let payload_type = json.get("type").and_then(|v| v.as_str()).unwrap_or("text"); - - let message_data = match payload_type { - "text" => json.get("text").and_then(|v| v.as_str()).unwrap_or_default().as_bytes().to_vec(), - "binary" => { - let binary_data = json.get("data").and_then(|v| v.as_str()).unwrap_or_default(); - base64::engine::general_purpose::STANDARD.decode(binary_data).unwrap_or_default() - } - _ => data.as_bytes().to_vec(), - }; - - return SignMessage { - chain: Chain::Ton, - sign_type: SignDigestType::TonPersonal, - data: message_data, - }; - } - - SignMessage { - chain: Chain::Ton, - sign_type: SignDigestType::TonPersonal, - data: data.as_bytes().to_vec(), - } - } - fn decode_siwe_message(&self, chain: Chain, raw_text: &str, message_data: &[u8]) -> Option { let message = SiweMessage::try_parse(raw_text)?; message.validate(chain).ok()?; @@ -295,12 +302,7 @@ impl WalletConnect { })? .to_string(); - let valid_until = json.get("valid_until").and_then(|v| v.as_i64()); - - Ok(WalletConnectTransaction::Ton { - data: actions::WCTonTransactionData { messages, valid_until }, - output_type, - }) + Ok(WalletConnectTransaction::Ton { messages, output_type }) } } } diff --git a/gemstone/src/wallet_connect/request_handler/ton.rs b/gemstone/src/wallet_connect/request_handler/ton.rs index 6d1c9151e..f86dbecea 100644 --- a/gemstone/src/wallet_connect/request_handler/ton.rs +++ b/gemstone/src/wallet_connect/request_handler/ton.rs @@ -49,14 +49,12 @@ mod tests { fn test_parse_sign_message() { let params = serde_json::from_str(r#"[{"type":"text","text":"Hello TON"}]"#).unwrap(); let action = TonRequestHandler::parse_sign_message(Chain::Ton, params).unwrap(); - match action { - WalletConnectAction::SignMessage { chain, sign_type, data } => { - assert_eq!(chain, Chain::Ton); - assert!(matches!(sign_type, SignDigestType::TonPersonal)); - assert!(data.contains("Hello TON")); - } - _ => panic!("Expected SignMessage action"), - } + let WalletConnectAction::SignMessage { chain, sign_type, data } = action else { + panic!("Expected SignMessage action") + }; + assert_eq!(chain, Chain::Ton); + assert_eq!(sign_type, SignDigestType::TonPersonal); + assert_eq!(data, r#"{"type":"text","text":"Hello TON"}"#); } #[test] @@ -73,21 +71,15 @@ mod tests { let params = serde_json::from_str(params_json).unwrap(); let action = TonRequestHandler::parse_send_transaction(Chain::Ton, params).unwrap(); - match action { - WalletConnectAction::SendTransaction { chain, transaction_type, data } => { - assert_eq!(chain, Chain::Ton); - assert!(matches!( - transaction_type, - WalletConnectTransactionType::Ton { - output_type: TransferDataOutputType::EncodedTransaction - } - )); - let parsed_data: serde_json::Value = serde_json::from_str(&data).expect("Data should be valid JSON"); - assert!(parsed_data.get("messages").is_some()); - assert_eq!(parsed_data.get("valid_until").and_then(|v| v.as_i64()), Some(1234567890)); - } - _ => panic!("Expected SendTransaction action"), - } + let WalletConnectAction::SendTransaction { chain, transaction_type, data } = action else { + panic!("Expected SendTransaction action") + }; + assert_eq!(chain, Chain::Ton); + let WalletConnectTransactionType::Ton { output_type: TransferDataOutputType::EncodedTransaction } = transaction_type else { + panic!("Expected Ton transaction type with EncodedTransaction output") + }; + let parsed_data: serde_json::Value = serde_json::from_str(&data).expect("Data should be valid JSON"); + assert!(parsed_data.get("messages").is_some()); } #[test] From a93d4f217f5a80550cd569ae0f898c65e5c16b26 Mon Sep 17 00:00:00 2001 From: Radmir <52320354+DRadmir@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:32:01 +0500 Subject: [PATCH 03/11] Refactor TON sign data to prepare data without signing --- Cargo.lock | 1 + crates/gem_ton/src/address.rs | 8 - .../gem_tron/src/provider/preload_mapper.rs | 5 +- crates/signer/Cargo.toml | 1 + crates/signer/src/lib.rs | 6 +- crates/signer/src/ton.rs | 171 +++++++++--------- gemstone/src/config/wallet_connect.rs | 6 +- gemstone/src/message/decoder.rs | 34 ++-- gemstone/src/signer/crypto.rs | 7 +- gemstone/src/wallet_connect/mod.rs | 58 ++++-- .../src/wallet_connect/request_handler/ton.rs | 5 +- 11 files changed, 167 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a0d04f47..edb7bc2fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6517,6 +6517,7 @@ dependencies = [ "k256", "serde", "serde_json", + "strum", "sui-sdk-types", "zeroize", ] diff --git a/crates/gem_ton/src/address.rs b/crates/gem_ton/src/address.rs index bbc5fa4c1..0cfcef4da 100644 --- a/crates/gem_ton/src/address.rs +++ b/crates/gem_ton/src/address.rs @@ -172,12 +172,4 @@ mod tests { assert_eq!(original_hex, decoded_hex); } - - #[test] - fn test_base64_to_hex_user_address() { - let base64 = "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"; - let hex = base64_to_hex_address(base64.to_string()).unwrap(); - - assert!(hex.starts_with("0:") || hex.starts_with("-1:")); - } } diff --git a/crates/gem_tron/src/provider/preload_mapper.rs b/crates/gem_tron/src/provider/preload_mapper.rs index d86cff092..094949163 100644 --- a/crates/gem_tron/src/provider/preload_mapper.rs +++ b/crates/gem_tron/src/provider/preload_mapper.rs @@ -119,7 +119,10 @@ mod tests { use primitives::delegation::DelegationValidator; fn chain_parameter(key: &str, value: i64) -> ChainParameter { - ChainParameter { key: key.to_string(), value: Some(value) } + ChainParameter { + key: key.to_string(), + value: Some(value), + } } fn account_usage(free_bandwidth: u64, staked_bandwidth: u64, available_energy: u64) -> TronAccountUsage { diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index fe3728da2..24291230b 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -14,5 +14,6 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } gem_hash = { path = "../gem_hash" } zeroize = { version = "1.8.2" } +strum = { workspace = true } [dev-dependencies] diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index d7a7f2beb..8847f95dc 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -18,7 +18,7 @@ use crate::sui::assemble_signature; pub use eip712::hash_typed_data as hash_eip712; pub use error::SignerError; pub use sui::SUI_PERSONAL_MESSAGE_SIGNATURE_LEN; -pub use ton::TonSignDataInput; +pub use ton::{TonSignDataPayload, TonSignDataResponse, TonSignDataType}; #[derive(Debug, Default)] pub struct Signer; @@ -61,10 +61,6 @@ impl Signer { let signature = Self::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key_vec.to_vec())?; Ok(hex::encode(signature)) } - - pub fn sign_ton_personal_message(input: TonSignDataInput, private_key: Vec) -> Result { - ton::sign_ton_personal_message(input, private_key) - } } #[cfg(test)] diff --git a/crates/signer/src/ton.rs b/crates/signer/src/ton.rs index 7c5a8a41e..c639ffa2e 100644 --- a/crates/signer/src/ton.rs +++ b/crates/signer/src/ton.rs @@ -1,31 +1,20 @@ -use std::time::{SystemTime, UNIX_EPOCH}; +use std::str::FromStr; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use ed25519_dalek::Signer as DalekSigner; use serde::{Deserialize, Serialize}; -use zeroize::Zeroizing; +use strum::{AsRefStr, EnumString}; -use crate::ed25519::signing_key_from_bytes; use crate::error::SignerError; -#[derive(Clone, Debug, PartialEq)] -enum TonSignDataType { +#[derive(Clone, Debug, PartialEq, AsRefStr, EnumString)] +#[strum(serialize_all = "lowercase")] +pub enum TonSignDataType { Text, Binary, Cell, } impl TonSignDataType { - fn as_str(&self) -> &'static str { - match self { - TonSignDataType::Text => "text", - TonSignDataType::Binary => "binary", - TonSignDataType::Cell => "cell", - } - } - - fn data_field(&self) -> &'static str { + pub fn data_field(&self) -> &'static str { match self { TonSignDataType::Text => "text", TonSignDataType::Binary => "bytes", @@ -43,33 +32,40 @@ struct TonSignDataPayloadRaw { cell: Option, } -struct TonSignDataPayload { - payload_type: TonSignDataType, - data: String, +pub struct TonSignDataPayload { + pub payload_type: TonSignDataType, + pub data: String, } impl TonSignDataPayload { - fn parse(json: &str) -> Result { + pub fn parse(json: &str) -> Result { let raw: TonSignDataPayloadRaw = serde_json::from_str(json)?; - let (payload_type, data) = match raw.payload_type.as_str() { - "text" => (TonSignDataType::Text, raw.text.ok_or("Missing text field")?), - "binary" => (TonSignDataType::Binary, raw.bytes.ok_or("Missing bytes field")?), - "cell" => (TonSignDataType::Cell, raw.cell.ok_or("Missing cell field")?), - _ => return Err(SignerError::new(format!("Unknown payload type: {}", raw.payload_type))), + let payload_type = TonSignDataType::from_str(&raw.payload_type).map_err(|_| SignerError::new(format!("Unknown payload type: {}", raw.payload_type)))?; + + let data = match payload_type { + TonSignDataType::Text => raw.text.ok_or("Missing text field")?, + TonSignDataType::Binary => raw.bytes.ok_or("Missing bytes field")?, + TonSignDataType::Cell => raw.cell.ok_or("Missing cell field")?, }; Ok(Self { payload_type, data }) } -} -pub struct TonSignDataInput { - pub domain: String, - pub payload: Vec, + pub fn hash(&self) -> Vec { + self.data.as_bytes().to_vec() + } + + pub fn to_json(&self) -> serde_json::Value { + serde_json::json!({ + "type": self.payload_type.as_ref(), + self.payload_type.data_field(): self.data, + }) + } } #[derive(Serialize)] -struct TonSignDataResponse { +pub struct TonSignDataResponse { signature: String, #[serde(rename = "publicKey")] public_key: String, @@ -78,38 +74,25 @@ struct TonSignDataResponse { payload: serde_json::Value, } -pub fn sign_ton_personal_message(input: TonSignDataInput, private_key: Vec) -> Result { - let private_key = Zeroizing::new(private_key); - let payload_str = std::str::from_utf8(&input.payload).map_err(|e| SignerError::new(e.to_string()))?; - let parsed = TonSignDataPayload::parse(payload_str)?; - - let signing_key = signing_key_from_bytes(&private_key)?; - let signature = signing_key.sign(parsed.data.as_bytes()); - - let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0); - - let payload = serde_json::json!({ - "type": parsed.payload_type.as_str(), - parsed.payload_type.data_field(): parsed.data, - }); - - let response = TonSignDataResponse { - signature: STANDARD.encode(signature.to_bytes()), - public_key: STANDARD.encode(signing_key.verifying_key().to_bytes()), - timestamp, - domain: input.domain, - payload, - }; +impl TonSignDataResponse { + pub fn new(signature: String, public_key: String, timestamp: u64, domain: String, payload: serde_json::Value) -> Self { + Self { + signature, + public_key, + timestamp, + domain, + payload, + } + } - Ok(serde_json::to_string(&response)?) + pub fn to_json(&self) -> Result { + serde_json::to_string(self).map_err(SignerError::from) + } } #[cfg(test)] mod tests { use super::*; - use ed25519_dalek::Verifier; - - const TEST_PRIVATE_KEY: &str = "1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34"; #[test] fn test_parse_payload_text() { @@ -118,46 +101,62 @@ mod tests { assert_eq!(parsed.payload_type, TonSignDataType::Text); assert_eq!(parsed.data, "Hello TON"); + assert_eq!(parsed.hash(), b"Hello TON".to_vec()); } #[test] - fn test_verify_signature() { - let private_key = hex::decode(TEST_PRIVATE_KEY).unwrap(); - let text = "Hello TON"; - let input = TonSignDataInput { - domain: "example.com".to_string(), - payload: format!(r#"{{"type":"text","text":"{}"}}"#, text).into_bytes(), - }; - - let result_json = sign_ton_personal_message(input, private_key.clone()).unwrap(); - let result: serde_json::Value = serde_json::from_str(&result_json).unwrap(); + fn test_parse_payload_binary() { + let json = r#"{"type":"binary","bytes":"SGVsbG8="}"#; + let parsed = TonSignDataPayload::parse(json).unwrap(); - let signing_key = signing_key_from_bytes(&private_key).unwrap(); - let public_key = signing_key.verifying_key(); + assert_eq!(parsed.payload_type, TonSignDataType::Binary); + assert_eq!(parsed.data, "SGVsbG8="); + } - let signature_base64 = result["signature"].as_str().unwrap(); - let signature_bytes = STANDARD.decode(signature_base64).unwrap(); - let signature = ed25519_dalek::Signature::from_slice(&signature_bytes).unwrap(); + #[test] + fn test_parse_payload_cell() { + let json = r#"{"type":"cell","cell":"te6c"}"#; + let parsed = TonSignDataPayload::parse(json).unwrap(); - assert!(public_key.verify(text.as_bytes(), &signature).is_ok()); - assert_eq!(result["publicKey"].as_str().unwrap(), STANDARD.encode(public_key.to_bytes())); - assert!(result["timestamp"].as_u64().unwrap() > 0); + assert_eq!(parsed.payload_type, TonSignDataType::Cell); + assert_eq!(parsed.data, "te6c"); } #[test] - fn test_response_format_text() { - let private_key = hex::decode(TEST_PRIVATE_KEY).unwrap(); - let input = TonSignDataInput { - domain: "react-app.walletconnect.com".to_string(), - payload: r#"{"type":"text","text":"Hello from WalletConnect TON"}"#.as_bytes().to_vec(), + fn test_payload_to_json() { + let payload = TonSignDataPayload { + payload_type: TonSignDataType::Text, + data: "Hello TON".to_string(), }; - let result_json = sign_ton_personal_message(input, private_key).unwrap(); - let result: serde_json::Value = serde_json::from_str(&result_json).unwrap(); + let json = payload.to_json(); + assert_eq!(json["type"], "text"); + assert_eq!(json["text"], "Hello TON"); + } + + #[test] + fn test_response_to_json() { + let payload = TonSignDataPayload { + payload_type: TonSignDataType::Text, + data: "Hello TON".to_string(), + }; - assert_eq!(result["domain"], "react-app.walletconnect.com"); - assert!(result["timestamp"].as_u64().unwrap() > 0); - assert_eq!(result["payload"]["type"], "text"); - assert_eq!(result["payload"]["text"], "Hello from WalletConnect TON"); + let response = TonSignDataResponse::new( + "c2lnbmF0dXJl".to_string(), + "cHVibGljS2V5".to_string(), + 1234567890, + "example.com".to_string(), + payload.to_json(), + ); + + let json = response.to_json().unwrap(); + let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); + + assert_eq!(parsed["signature"], "c2lnbmF0dXJl"); + assert_eq!(parsed["publicKey"], "cHVibGljS2V5"); + assert_eq!(parsed["timestamp"], 1234567890); + assert_eq!(parsed["domain"], "example.com"); + assert_eq!(parsed["payload"]["type"], "text"); + assert_eq!(parsed["payload"]["text"], "Hello TON"); } } diff --git a/gemstone/src/config/wallet_connect.rs b/gemstone/src/config/wallet_connect.rs index 674d0525f..7e2f44ced 100644 --- a/gemstone/src/config/wallet_connect.rs +++ b/gemstone/src/config/wallet_connect.rs @@ -6,7 +6,11 @@ pub struct WalletConnectConfig { } pub fn get_wallet_connect_config() -> WalletConnectConfig { - let chains: Vec = [vec![Chain::Solana, Chain::Sui, Chain::Ton], EVMChain::all().iter().map(|x| x.to_chain()).collect()].concat(); + let chains: Vec = [ + vec![Chain::Solana, Chain::Sui, Chain::Ton], + EVMChain::all().iter().map(|x| x.to_chain()).collect(), + ] + .concat(); WalletConnectConfig { chains: chains.into_iter().map(|x| x.to_string()).collect(), diff --git a/gemstone/src/message/decoder.rs b/gemstone/src/message/decoder.rs index 699e7b08c..b235eac5c 100644 --- a/gemstone/src/message/decoder.rs +++ b/gemstone/src/message/decoder.rs @@ -2,6 +2,7 @@ use alloy_primitives::{eip191_hash_message, hex::encode_prefixed}; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; use bs58; +use signer::{TonSignDataPayload, TonSignDataResponse}; use super::{ eip712::GemEIP712Message, @@ -41,19 +42,10 @@ impl SignMessageDecoder { } SignDigestType::TonPersonal => { let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8"))?; - let Some(json) = serde_json::from_str::(&string).ok() else { + let Ok(payload) = TonSignDataPayload::parse(&string) else { return Ok(MessagePreview::Text(string)); }; - let Some(payload_type) = json.get("type").and_then(|v| v.as_str()) else { - return Ok(MessagePreview::Text(string)); - }; - let preview = match payload_type { - "text" => json.get("text").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - "binary" => json.get("bytes").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - "cell" => json.get("cell").and_then(|v| v.as_str()).unwrap_or_default().to_string(), - _ => string, - }; - Ok(MessagePreview::Text(preview)) + Ok(MessagePreview::Text(payload.data)) } SignDigestType::Eip712 => { let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8 string for EIP712"))?; @@ -100,7 +92,16 @@ impl SignMessageDecoder { pub fn hash(&self) -> Vec { match &self.message.sign_type { - SignDigestType::SuiPersonal | SignDigestType::TonPersonal => self.message.data.clone(), + SignDigestType::SuiPersonal => self.message.data.clone(), + SignDigestType::TonPersonal => { + let Ok(string) = String::from_utf8(self.message.data.clone()) else { + return Vec::new(); + }; + let Ok(payload) = TonSignDataPayload::parse(&string) else { + return Vec::new(); + }; + payload.hash() + } SignDigestType::Eip191 | SignDigestType::Siwe => eip191_hash_message(&self.message.data).to_vec(), SignDigestType::Eip712 => match std::str::from_utf8(&self.message.data) { Ok(json) => hash_eip712(json).map(|digest| digest.to_vec()).unwrap_or_default(), @@ -131,6 +132,15 @@ impl SignMessageDecoder { SignDigestType::Base58 => bs58::encode(data).into_string(), } } + + pub fn get_ton_result(&self, signature: &[u8], public_key: &[u8], timestamp: u64, domain: String) -> Result { + let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8"))?; + let payload = TonSignDataPayload::parse(&string).map_err(|e| GemstoneError::from(e.to_string()))?; + + let response = TonSignDataResponse::new(BASE64.encode(signature), BASE64.encode(public_key), timestamp, domain, payload.to_json()); + + response.to_json().map_err(|e| GemstoneError::from(e.to_string())) + } } #[cfg(test)] diff --git a/gemstone/src/signer/crypto.rs b/gemstone/src/signer/crypto.rs index 13d9f2012..eb17da8a5 100644 --- a/gemstone/src/signer/crypto.rs +++ b/gemstone/src/signer/crypto.rs @@ -1,5 +1,5 @@ use gem_ton::address::base64_to_hex_address; -use signer::{SignatureScheme as GemSignatureScheme, Signer, TonSignDataInput as SignerTonSignDataInput}; +use signer::{SignatureScheme as GemSignatureScheme, Signer}; use crate::GemstoneError; @@ -25,11 +25,6 @@ impl CryptoSigner { Signer::sign_digest(scheme, digest, private_key).map_err(GemstoneError::from) } - pub fn sign_ton_personal_message(&self, domain: String, payload: Vec, private_key: Vec) -> Result { - let signer_input = SignerTonSignDataInput { domain, payload }; - Signer::sign_ton_personal_message(signer_input, private_key).map_err(GemstoneError::from) - } - pub fn ton_base64_to_raw_address(&self, base64_address: String) -> Result { base64_to_hex_address(base64_address).map_err(|e| GemstoneError::AnyError { msg: e.to_string() }) } diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index 2c09baae0..bdd433479 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use hex::FromHex; use primitives::{Chain, WCEthereumTransaction, WalletConnectRequest, WalletConnectionVerificationStatus}; +use signer::TonSignDataType; fn current_timestamp() -> i64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0) @@ -91,11 +92,31 @@ mod tests { fn validate_ton_sign_message() { let wallet_connect = WalletConnect::new(); - assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"text":"Hello"}"#.to_string()).is_err()); - assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"unknown"}"#.to_string()).is_err()); - assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"text","text":"Hello"}"#.to_string()).is_ok()); - assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"binary","bytes":"SGVsbG8="}"#.to_string()).is_ok()); - assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"cell","cell":"te6c"}"#.to_string()).is_ok()); + assert!( + wallet_connect + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"text":"Hello"}"#.to_string()) + .is_err() + ); + assert!( + wallet_connect + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"unknown"}"#.to_string()) + .is_err() + ); + assert!( + wallet_connect + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"text","text":"Hello"}"#.to_string()) + .is_ok() + ); + assert!( + wallet_connect + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"binary","bytes":"SGVsbG8="}"#.to_string()) + .is_ok() + ); + assert!( + wallet_connect + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"cell","cell":"te6c"}"#.to_string()) + .is_ok() + ); } #[test] @@ -105,8 +126,16 @@ mod tests { output_type: primitives::TransferDataOutputType::EncodedTransaction, }; - assert!(wallet_connect.validate_send_transaction(ton_type.clone(), r#"{"valid_until": 1234567890, "messages": []}"#.to_string()).is_err()); - assert!(wallet_connect.validate_send_transaction(ton_type.clone(), r#"{"valid_until": 9999999999, "messages": []}"#.to_string()).is_ok()); + assert!( + wallet_connect + .validate_send_transaction(ton_type.clone(), r#"{"valid_until": 1234567890, "messages": []}"#.to_string()) + .is_err() + ); + assert!( + wallet_connect + .validate_send_transaction(ton_type.clone(), r#"{"valid_until": 9999999999, "messages": []}"#.to_string()) + .is_ok() + ); assert!(wallet_connect.validate_send_transaction(ton_type, r#"{"messages": []}"#.to_string()).is_ok()); } } @@ -172,17 +201,16 @@ impl WalletConnect { gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } SignDigestType::TonPersonal => { - let json: serde_json::Value = - serde_json::from_str(&data).map_err(|_| crate::GemstoneError::AnyError { msg: "Invalid JSON".to_string() })?; + let json: serde_json::Value = serde_json::from_str(&data).map_err(|_| crate::GemstoneError::AnyError { + msg: "Invalid JSON".to_string(), + })?; let payload_type = json.get("type").and_then(|v| v.as_str()).ok_or_else(|| crate::GemstoneError::AnyError { msg: "Missing type field".to_string(), })?; - match payload_type { - "text" | "binary" | "cell" => Ok(()), - _ => Err(crate::GemstoneError::AnyError { - msg: format!("Unsupported payload type: {}", payload_type), - }), - } + TonSignDataType::from_str(payload_type).map_err(|_| crate::GemstoneError::AnyError { + msg: format!("Unsupported payload type: {}", payload_type), + })?; + Ok(()) } SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe => Ok(()), } diff --git a/gemstone/src/wallet_connect/request_handler/ton.rs b/gemstone/src/wallet_connect/request_handler/ton.rs index f86dbecea..27de5bb78 100644 --- a/gemstone/src/wallet_connect/request_handler/ton.rs +++ b/gemstone/src/wallet_connect/request_handler/ton.rs @@ -75,7 +75,10 @@ mod tests { panic!("Expected SendTransaction action") }; assert_eq!(chain, Chain::Ton); - let WalletConnectTransactionType::Ton { output_type: TransferDataOutputType::EncodedTransaction } = transaction_type else { + let WalletConnectTransactionType::Ton { + output_type: TransferDataOutputType::EncodedTransaction, + } = transaction_type + else { panic!("Expected Ton transaction type with EncodedTransaction output") }; let parsed_data: serde_json::Value = serde_json::from_str(&data).expect("Data should be valid JSON"); From 037f01f778397cf649046ff74ac3bb4fdcd9342f Mon Sep 17 00:00:00 2001 From: Radmir <52320354+DRadmir@users.noreply.github.com> Date: Wed, 10 Dec 2025 10:42:09 +0500 Subject: [PATCH 04/11] Refactor TON response handler tests --- gemstone/src/wallet_connect/response_handler.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/gemstone/src/wallet_connect/response_handler.rs b/gemstone/src/wallet_connect/response_handler.rs index 7585266e4..309bc1501 100644 --- a/gemstone/src/wallet_connect/response_handler.rs +++ b/gemstone/src/wallet_connect/response_handler.rs @@ -160,12 +160,10 @@ mod tests { #[test] fn test_encode_sign_message_ton() { let ton_json = r#"{"signature":"tonsig123","timestamp":1700000000}"#.to_string(); - let result = WalletConnectResponseHandler::encode_sign_message(ChainType::Ton, ton_json); + let result = WalletConnectResponseHandler::encode_sign_message(ChainType::Ton, ton_json.clone()); match result { WalletConnectResponseType::Object { json } => { - assert!(json.contains("\"signature\"")); - assert!(json.contains("tonsig123")); - assert!(json.contains("\"timestamp\"")); + assert_eq!(json, ton_json); } _ => panic!("Expected Object response for Ton"), } @@ -176,8 +174,8 @@ mod tests { let result = WalletConnectResponseHandler::encode_sign_transaction(ChainType::Ton, "tontxsig".to_string()); match result { WalletConnectResponseType::Object { json } => { - assert!(json.contains("\"signature\"")); - assert!(json.contains("tontxsig")); + let parsed: serde_json::Value = serde_json::from_str(&json).expect("JSON should be valid"); + assert_eq!(parsed.get("signature").and_then(|v| v.as_str()), Some("tontxsig")); } _ => panic!("Expected Object response for Ton"), } From 86abc0856feed9867dd4e72f250c423fffcfebf9 Mon Sep 17 00:00:00 2001 From: Radmir <52320354+DRadmir@users.noreply.github.com> Date: Wed, 10 Dec 2025 21:40:11 +0500 Subject: [PATCH 05/11] Unit tests fixes --- .../src/wallet_connect/response_handler.rs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/gemstone/src/wallet_connect/response_handler.rs b/gemstone/src/wallet_connect/response_handler.rs index 309bc1501..e884df76c 100644 --- a/gemstone/src/wallet_connect/response_handler.rs +++ b/gemstone/src/wallet_connect/response_handler.rs @@ -80,7 +80,10 @@ mod tests { #[test] fn test_encode_sign_message_ethereum() { let result = WalletConnectResponseHandler::encode_sign_message(ChainType::Ethereum, "0xsignature".to_string()); - assert!(matches!(result, WalletConnectResponseType::String { value } if value == "0xsignature")); + let WalletConnectResponseType::String { value } = result else { + panic!("Expected String response for Ethereum") + }; + assert_eq!(value, "0xsignature"); } #[test] @@ -110,7 +113,10 @@ mod tests { #[test] fn test_encode_sign_transaction_ethereum() { let result = WalletConnectResponseHandler::encode_sign_transaction(ChainType::Ethereum, "0xtxid".to_string()); - assert!(matches!(result, WalletConnectResponseType::String { value } if value == "0xtxid")); + let WalletConnectResponseType::String { value } = result else { + panic!("Expected String response for Ethereum") + }; + assert_eq!(value, "0xtxid"); } #[test] @@ -142,7 +148,10 @@ mod tests { #[test] fn test_encode_send_transaction_ethereum() { let result = WalletConnectResponseHandler::encode_send_transaction(ChainType::Ethereum, "0xhash".to_string()); - assert!(matches!(result, WalletConnectResponseType::String { value } if value == "0xhash")); + let WalletConnectResponseType::String { value } = result else { + panic!("Expected String response for Ethereum") + }; + assert_eq!(value, "0xhash"); } #[test] @@ -172,18 +181,18 @@ mod tests { #[test] fn test_encode_sign_transaction_ton() { let result = WalletConnectResponseHandler::encode_sign_transaction(ChainType::Ton, "tontxsig".to_string()); - match result { - WalletConnectResponseType::Object { json } => { - let parsed: serde_json::Value = serde_json::from_str(&json).expect("JSON should be valid"); - assert_eq!(parsed.get("signature").and_then(|v| v.as_str()), Some("tontxsig")); - } - _ => panic!("Expected Object response for Ton"), - } + let WalletConnectResponseType::Object { json } = result else { + panic!("Expected Object response for Ton") + }; + assert_eq!(json, r#"{"signature":"tontxsig"}"#); } #[test] fn test_encode_send_transaction_ton() { let result = WalletConnectResponseHandler::encode_send_transaction(ChainType::Ton, "tonhash123".to_string()); - assert!(matches!(result, WalletConnectResponseType::String { value } if value == "tonhash123")); + let WalletConnectResponseType::String { value } = result else { + panic!("Expected String response for Ton") + }; + assert_eq!(value, "tonhash123"); } } From 26904110624e286fe6796ad7cb9e4ad49b07935c Mon Sep 17 00:00:00 2001 From: Radmir <52320354+DRadmir@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:42:20 +0500 Subject: [PATCH 06/11] Add TON WalletConnect signing support with domain parameter - Add domain field to WalletConnectRequest - Create TonSignMessageData struct to wrap payload and domain - Update ChainRequestHandler trait to pass domain to parse_sign_message - Update all handlers (ethereum, solana, sui, ton) with domain parameter - TON handler extracts host from URL and creates TonSignMessageData - Add sign_ton_personal to Signer for TON Ed25519 signing - Update MessageSigner with TonPersonal support (preview, hash, sign) - TON sign response includes signature, publicKey, timestamp, domain, payload --- crates/primitives/src/lib.rs | 2 +- crates/primitives/src/wallet_connect.rs | 11 +--- crates/signer/src/lib.rs | 40 +++++++++++- crates/signer/src/ton.rs | 47 ++++++++++++++ gemstone/src/message/signer.rs | 63 ++++++++++++++++++- gemstone/src/wallet_connect/handler_traits.rs | 2 +- gemstone/src/wallet_connect/mod.rs | 37 ++++++++--- .../request_handler/ethereum.rs | 6 +- .../src/wallet_connect/request_handler/mod.rs | 12 ++-- .../wallet_connect/request_handler/solana.rs | 4 +- .../src/wallet_connect/request_handler/sui.rs | 4 +- .../src/wallet_connect/request_handler/ton.rs | 37 +++++++++-- 12 files changed, 223 insertions(+), 42 deletions(-) diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index fbd5e04e4..92980c3b0 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -115,7 +115,7 @@ pub use self::transaction_metadata_types::{ pub mod wallet_connect_namespace; pub use self::wallet_connect_namespace::WalletConnectCAIP2; pub mod wallet_connect; -pub use self::wallet_connect::{WCEthereumTransaction, WCTonMessage, WalletConnectRequest}; +pub use self::wallet_connect::{WCEthereumTransaction, WalletConnectRequest}; pub mod account; pub use self::account::Account; pub mod wallet; diff --git a/crates/primitives/src/wallet_connect.rs b/crates/primitives/src/wallet_connect.rs index 954f9644c..3d8d34e0f 100644 --- a/crates/primitives/src/wallet_connect.rs +++ b/crates/primitives/src/wallet_connect.rs @@ -18,16 +18,6 @@ pub struct WCEthereumTransaction { pub data: Option, } -#[derive(Debug, Serialize, Deserialize)] -#[typeshare(swift = "Equatable, Hashable, Sendable")] -#[serde(rename_all = "camelCase")] -pub struct WCTonMessage { - pub address: String, - pub amount: String, - pub payload: Option, - pub state_init: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WalletConnectRequest { @@ -35,4 +25,5 @@ pub struct WalletConnectRequest { pub method: String, pub params: String, pub chain_id: Option, + pub domain: String, } diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index f194a81a7..2c085bbab 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -18,7 +18,9 @@ use crate::sui::assemble_signature; pub use eip712::hash_typed_data as hash_eip712; pub use error::SignerError; pub use sui::SUI_PERSONAL_MESSAGE_SIGNATURE_LEN; -pub use ton::{TonSignDataPayload, TonSignDataResponse, TonSignDataType}; +pub use ton::{TonSignDataPayload, TonSignDataResponse, TonSignDataType, TonSignMessageData}; + +use crate::ton::TonSignMessageData as TonSignMessageDataInternal; #[derive(Debug, Default)] pub struct Signer; @@ -60,6 +62,20 @@ impl Signer { let signature = Self::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key_vec.to_vec())?; Ok(hex::encode(signature)) } + + pub fn sign_ton_personal(data: &[u8], private_key: &[u8]) -> Result<(Vec, Vec), SignerError> { + let ton_data = TonSignMessageDataInternal::from_bytes(data)?; + let payload = ton_data.get_payload()?; + let digest = payload.hash(); + + let private_key = Zeroizing::new(private_key.to_vec()); + let signing_key = signing_key_from_bytes(&private_key)?; + let signature = signing_key.sign(digest.as_slice()); + let signature_bytes = signature.to_bytes().to_vec(); + let public_key_bytes = signing_key.verifying_key().to_bytes().to_vec(); + + Ok((signature_bytes, public_key_bytes)) + } } #[cfg(test)] @@ -106,4 +122,26 @@ mod tests { let result = signing_key_from_bytes(&[0u8; 16]); assert!(result.is_err()); } + + #[test] + fn test_sign_ton_personal() { + let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); + let data = ton_data.to_bytes(); + + let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").expect("valid hex"); + + let (signature, public_key) = Signer::sign_ton_personal(&data, &private_key).expect("signing succeeds"); + + assert_eq!(signature.len(), 64, "Ed25519 signature should be 64 bytes"); + assert_eq!(public_key.len(), 32, "Ed25519 public key should be 32 bytes"); + + let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key.try_into().expect("32 byte secret key"); + let signing_key = SigningKey::from_bytes(&key_bytes); + assert_eq!(public_key, signing_key.verifying_key().as_bytes(), "public key should match"); + + let signature = Signature::from_bytes(signature.as_slice().try_into().expect("64 byte signature")); + let digest = b"Hello TON"; + signing_key.verifying_key().verify(digest, &signature).expect("signature verifies"); + } } diff --git a/crates/signer/src/ton.rs b/crates/signer/src/ton.rs index c639ffa2e..361de7e63 100644 --- a/crates/signer/src/ton.rs +++ b/crates/signer/src/ton.rs @@ -74,6 +74,31 @@ pub struct TonSignDataResponse { payload: serde_json::Value, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TonSignMessageData { + pub payload: serde_json::Value, + pub domain: String, +} + +impl TonSignMessageData { + pub fn new(payload: serde_json::Value, domain: String) -> Self { + Self { payload, domain } + } + + pub fn from_bytes(data: &[u8]) -> Result { + serde_json::from_slice(data).map_err(SignerError::from) + } + + pub fn to_bytes(&self) -> Vec { + serde_json::to_vec(self).unwrap_or_default() + } + + pub fn get_payload(&self) -> Result { + let json = serde_json::to_string(&self.payload)?; + TonSignDataPayload::parse(&json) + } +} + impl TonSignDataResponse { pub fn new(signature: String, public_key: String, timestamp: u64, domain: String, payload: serde_json::Value) -> Self { Self { @@ -159,4 +184,26 @@ mod tests { assert_eq!(parsed["payload"]["type"], "text"); assert_eq!(parsed["payload"]["text"], "Hello TON"); } + + #[test] + fn test_ton_sign_message_data_roundtrip() { + let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let data = TonSignMessageData::new(payload.clone(), "example.com".to_string()); + + let bytes = data.to_bytes(); + let parsed = TonSignMessageData::from_bytes(&bytes).unwrap(); + + assert_eq!(parsed.payload, payload); + assert_eq!(parsed.domain, "example.com"); + } + + #[test] + fn test_ton_sign_message_data_get_payload() { + let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let data = TonSignMessageData::new(payload, "example.com".to_string()); + + let parsed_payload = data.get_payload().unwrap(); + assert_eq!(parsed_payload.payload_type, TonSignDataType::Text); + assert_eq!(parsed_payload.data, "Hello TON"); + } } diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index 7a18c1b4e..197278d11 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -4,6 +4,7 @@ use alloy_primitives::{eip191_hash_message, hex::encode_prefixed}; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; use bs58; +use signer::{TonSignDataResponse, TonSignMessageData}; use sui_types::PersonalMessage; use super::{ @@ -44,6 +45,16 @@ impl MessageSigner { let preview = string.unwrap_or(encode_prefixed(&self.message.data)); Ok(MessagePreview::Text(preview)) } + SignDigestType::TonPersonal => { + let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8"))?; + let Ok(ton_data) = TonSignMessageData::from_bytes(string.as_bytes()) else { + return Ok(MessagePreview::Text(string)); + }; + let Ok(payload) = ton_data.get_payload() else { + return Ok(MessagePreview::Text(string)); + }; + Ok(MessagePreview::Text(payload.data)) + } SignDigestType::Eip712 => { let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8 string for EIP712"))?; if string.is_empty() { @@ -75,7 +86,7 @@ impl MessageSigner { pub fn plain_preview(&self) -> String { match self.message.sign_type { - SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::Base58 => match self.preview() { + SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::TonPersonal => match self.preview() { Ok(MessagePreview::Text(preview)) => preview, _ => "".to_string(), }, @@ -93,6 +104,18 @@ impl MessageSigner { let message = PersonalMessage(Cow::Borrowed(&self.message.data)); message.signing_digest().to_vec() } + SignDigestType::TonPersonal => { + let Ok(string) = String::from_utf8(self.message.data.clone()) else { + return Vec::new(); + }; + let Ok(ton_data) = TonSignMessageData::from_bytes(string.as_bytes()) else { + return Vec::new(); + }; + let Ok(payload) = ton_data.get_payload() else { + return Vec::new(); + }; + payload.hash() + } SignDigestType::Eip191 | SignDigestType::Siwe => eip191_hash_message(&self.message.data).to_vec(), SignDigestType::Eip712 => match std::str::from_utf8(&self.message.data) { Ok(json) => hash_eip712(json).map(|digest| digest.to_vec()).unwrap_or_default(), @@ -119,16 +142,34 @@ impl MessageSigner { } encode_prefixed(&signature) } - SignDigestType::SuiPersonal => BASE64.encode(data), + SignDigestType::SuiPersonal | SignDigestType::TonPersonal => BASE64.encode(data), SignDigestType::Base58 => bs58::encode(data).into_string(), } } + pub fn get_ton_result(&self, signature: &[u8], public_key: &[u8]) -> Result { + let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8"))?; + let ton_data = TonSignMessageData::from_bytes(string.as_bytes()).map_err(|e| GemstoneError::from(e.to_string()))?; + let payload = ton_data.get_payload().map_err(|e| GemstoneError::from(e.to_string()))?; + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + + let response = TonSignDataResponse::new(BASE64.encode(signature), BASE64.encode(public_key), timestamp, ton_data.domain, payload.to_json()); + + response.to_json().map_err(|e| GemstoneError::from(e.to_string())) + } + pub fn sign(&self, private_key: Vec) -> Result { let private_key = Zeroizing::new(private_key); let hash = self.hash(); match &self.message.sign_type { SignDigestType::SuiPersonal => Signer::sign_sui_digest(&hash, &private_key).map_err(GemstoneError::from), + SignDigestType::TonPersonal => { + let (signature, public_key) = Signer::sign_ton_personal(&self.message.data, &private_key)?; + self.get_ton_result(&signature, &public_key) + } SignDigestType::Eip191 | SignDigestType::Eip712 | SignDigestType::Siwe => { let signed = Signer::sign_digest(SignatureScheme::Secp256k1, hash, private_key.to_vec())?; Ok(self.get_result(&signed)) @@ -453,4 +494,22 @@ mod tests { assert_eq!(decoder.plain_preview(), message); } + + #[test] + fn test_ton_personal_preview() { + let ton_data = TonSignMessageData::new(serde_json::json!({"type": "text", "text": "Hello TON", "from": "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}), "example.com".to_string()); + let data = String::from_utf8(ton_data.to_bytes()).unwrap(); + let decoder = MessageSigner::new(SignMessage { + chain: Chain::Ton, + sign_type: SignDigestType::TonPersonal, + data: data.as_bytes().to_vec(), + }); + + match decoder.preview() { + Ok(MessagePreview::Text(preview)) => assert_eq!(preview, "Hello TON"), + other => panic!("Unexpected result: {other:?}"), + } + + assert_eq!(decoder.plain_preview(), "Hello TON"); + } } diff --git a/gemstone/src/wallet_connect/handler_traits.rs b/gemstone/src/wallet_connect/handler_traits.rs index 2476686ff..37c061262 100644 --- a/gemstone/src/wallet_connect/handler_traits.rs +++ b/gemstone/src/wallet_connect/handler_traits.rs @@ -4,7 +4,7 @@ use primitives::Chain; use serde_json::Value; pub trait ChainRequestHandler { - fn parse_sign_message(chain: Chain, params: Value) -> Result; + fn parse_sign_message(chain: Chain, params: Value, domain: &str) -> Result; fn parse_sign_transaction(chain: Chain, params: Value) -> Result; fn parse_send_transaction(chain: Chain, params: Value) -> Result; } diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index bdd433479..ca7889cbe 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -90,31 +90,47 @@ mod tests { #[test] fn validate_ton_sign_message() { + use signer::TonSignMessageData; + let wallet_connect = WalletConnect::new(); + // Missing type field in payload + let ton_data = TonSignMessageData::new(serde_json::json!({"text":"Hello"}), "example.com".to_string()); assert!( wallet_connect - .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"text":"Hello"}"#.to_string()) + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) .is_err() ); + + // Unknown type + let ton_data = TonSignMessageData::new(serde_json::json!({"type":"unknown"}), "example.com".to_string()); assert!( wallet_connect - .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"unknown"}"#.to_string()) + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) .is_err() ); + + // Valid text type + let ton_data = TonSignMessageData::new(serde_json::json!({"type":"text","text":"Hello"}), "example.com".to_string()); assert!( wallet_connect - .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"text","text":"Hello"}"#.to_string()) + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) .is_ok() ); + + // Valid binary type + let ton_data = TonSignMessageData::new(serde_json::json!({"type":"binary","bytes":"SGVsbG8="}), "example.com".to_string()); assert!( wallet_connect - .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"binary","bytes":"SGVsbG8="}"#.to_string()) + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) .is_ok() ); + + // Valid cell type + let ton_data = TonSignMessageData::new(serde_json::json!({"type":"cell","cell":"te6c"}), "example.com".to_string()); assert!( wallet_connect - .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, r#"{"type":"cell","cell":"te6c"}"#.to_string()) + .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) .is_ok() ); } @@ -161,12 +177,13 @@ impl WalletConnect { Some(primitives::WalletConnectCAIP2::get_chain(caip2, caip10)?.to_string()) } - pub fn parse_request(&self, topic: String, method: String, params: String, chain_id: String) -> Result { + pub fn parse_request(&self, topic: String, method: String, params: String, chain_id: String, domain: String) -> Result { let request = WalletConnectRequest { topic, method, params, chain_id: Some(chain_id), + domain, }; WalletConnectRequestHandler::parse_request(request).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } @@ -201,11 +218,11 @@ impl WalletConnect { gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } SignDigestType::TonPersonal => { - let json: serde_json::Value = serde_json::from_str(&data).map_err(|_| crate::GemstoneError::AnyError { - msg: "Invalid JSON".to_string(), + let ton_data = signer::TonSignMessageData::from_bytes(data.as_bytes()).map_err(|e| crate::GemstoneError::AnyError { + msg: format!("Invalid TonSignMessageData: {}", e), })?; - let payload_type = json.get("type").and_then(|v| v.as_str()).ok_or_else(|| crate::GemstoneError::AnyError { - msg: "Missing type field".to_string(), + let payload_type = ton_data.payload.get("type").and_then(|v| v.as_str()).ok_or_else(|| crate::GemstoneError::AnyError { + msg: "Missing type field in payload".to_string(), })?; TonSignDataType::from_str(payload_type).map_err(|_| crate::GemstoneError::AnyError { msg: format!("Unsupported payload type: {}", payload_type), diff --git a/gemstone/src/wallet_connect/request_handler/ethereum.rs b/gemstone/src/wallet_connect/request_handler/ethereum.rs index a385d2ce2..3595263a9 100644 --- a/gemstone/src/wallet_connect/request_handler/ethereum.rs +++ b/gemstone/src/wallet_connect/request_handler/ethereum.rs @@ -7,7 +7,7 @@ use serde_json::Value; pub struct EthereumRequestHandler; impl ChainRequestHandler for EthereumRequestHandler { - fn parse_sign_message(chain: Chain, params: Value) -> Result { + fn parse_sign_message(chain: Chain, params: Value, _domain: &str) -> Result { let params_array = params.as_array().ok_or("Invalid params format")?; let data = params_array.first().and_then(|v| v.as_str()).ok_or("Missing data parameter")?.to_string(); @@ -69,7 +69,7 @@ mod tests { fn test_parse_personal_sign() { use crate::wallet_connect::handler_traits::ChainRequestHandler; let params = serde_json::from_str(r#"["0x48656c6c6f"]"#).unwrap(); - let action = EthereumRequestHandler::parse_sign_message(Chain::Ethereum, params).unwrap(); + let action = EthereumRequestHandler::parse_sign_message(Chain::Ethereum, params, "example.com").unwrap(); match action { WalletConnectAction::SignMessage { chain, sign_type, data } => { assert_eq!(chain, Chain::Ethereum); @@ -150,7 +150,7 @@ mod tests { ] .join("\n"); let params = serde_json::json!([message.clone()]); - let action = EthereumRequestHandler::parse_sign_message(Chain::Ethereum, params).unwrap(); + let action = EthereumRequestHandler::parse_sign_message(Chain::Ethereum, params, "example.com").unwrap(); match action { WalletConnectAction::SignMessage { sign_type, data, .. } => { assert!(matches!(sign_type, SignDigestType::Eip191)); diff --git a/gemstone/src/wallet_connect/request_handler/mod.rs b/gemstone/src/wallet_connect/request_handler/mod.rs index e8de90b71..ef8c6304c 100644 --- a/gemstone/src/wallet_connect/request_handler/mod.rs +++ b/gemstone/src/wallet_connect/request_handler/mod.rs @@ -20,10 +20,12 @@ impl WalletConnectRequestHandler { .map_err(|_| format!("Unsupported method: {}", request.method))?; let params = serde_json::from_str::(&request.params).map_err(|e| format!("Failed to parse params: {}", e))?; + let domain = &request.domain; + match method { WalletConnectionMethods::PersonalSign => { let chain = Self::resolve_chain(request.chain_id)?; - EthereumRequestHandler::parse_sign_message(chain, params) + EthereumRequestHandler::parse_sign_message(chain, params, domain) } WalletConnectionMethods::EthSignTypedData | WalletConnectionMethods::EthSignTypedDataV4 => { let chain = Self::resolve_chain(request.chain_id)?; @@ -47,14 +49,14 @@ impl WalletConnectRequestHandler { WalletConnectionMethods::WalletSwitchEthereumChain => Ok(WalletConnectAction::ChainOperation { operation: WalletConnectChainOperation::SwitchChain, }), - WalletConnectionMethods::SolanaSignMessage => SolanaRequestHandler::parse_sign_message(Chain::Solana, params), + WalletConnectionMethods::SolanaSignMessage => SolanaRequestHandler::parse_sign_message(Chain::Solana, params, domain), WalletConnectionMethods::SolanaSignTransaction => SolanaRequestHandler::parse_sign_transaction(Chain::Solana, params), WalletConnectionMethods::SolanaSignAndSendTransaction => SolanaRequestHandler::parse_send_transaction(Chain::Solana, params), WalletConnectionMethods::SolanaSignAllTransactions => SolanaRequestHandler::parse_sign_all_transactions(params), - WalletConnectionMethods::SuiSignPersonalMessage => SuiRequestHandler::parse_sign_message(Chain::Sui, params), + WalletConnectionMethods::SuiSignPersonalMessage => SuiRequestHandler::parse_sign_message(Chain::Sui, params, domain), WalletConnectionMethods::SuiSignTransaction => SuiRequestHandler::parse_sign_transaction(Chain::Sui, params), WalletConnectionMethods::SuiSignAndExecuteTransaction => SuiRequestHandler::parse_send_transaction(Chain::Sui, params), - WalletConnectionMethods::TonSignData => TonRequestHandler::parse_sign_message(Chain::Ton, params), + WalletConnectionMethods::TonSignData => TonRequestHandler::parse_sign_message(Chain::Ton, params, domain), WalletConnectionMethods::TonSendMessage => TonRequestHandler::parse_send_transaction(Chain::Ton, params), } } @@ -75,6 +77,7 @@ mod tests { method: "unknown_method".to_string(), params: "{}".to_string(), chain_id: None, + domain: "example.com".to_string(), }; let result = WalletConnectRequestHandler::parse_request(request); @@ -88,6 +91,7 @@ mod tests { method: "wallet_addEthereumChain".to_string(), params: "{}".to_string(), chain_id: None, + domain: "example.com".to_string(), }; let action = WalletConnectRequestHandler::parse_request(request).unwrap(); diff --git a/gemstone/src/wallet_connect/request_handler/solana.rs b/gemstone/src/wallet_connect/request_handler/solana.rs index 38da4d7f6..ae0f0002d 100644 --- a/gemstone/src/wallet_connect/request_handler/solana.rs +++ b/gemstone/src/wallet_connect/request_handler/solana.rs @@ -7,7 +7,7 @@ use serde_json::Value; pub struct SolanaRequestHandler; impl ChainRequestHandler for SolanaRequestHandler { - fn parse_sign_message(_chain: Chain, params: Value) -> Result { + fn parse_sign_message(_chain: Chain, params: Value, _domain: &str) -> Result { let message = params.get("message").and_then(|v| v.as_str()).ok_or("Missing message parameter")?.to_string(); Ok(WalletConnectAction::SignMessage { @@ -64,7 +64,7 @@ mod tests { #[test] fn test_parse_sign_message() { let params = serde_json::from_str(r#"{"message":"Hello"}"#).unwrap(); - let action = SolanaRequestHandler::parse_sign_message(Chain::Solana, params).unwrap(); + let action = SolanaRequestHandler::parse_sign_message(Chain::Solana, params, "example.com").unwrap(); match action { WalletConnectAction::SignMessage { chain, sign_type, data } => { assert_eq!(chain, Chain::Solana); diff --git a/gemstone/src/wallet_connect/request_handler/sui.rs b/gemstone/src/wallet_connect/request_handler/sui.rs index eb8bab3e8..2d9bbf66c 100644 --- a/gemstone/src/wallet_connect/request_handler/sui.rs +++ b/gemstone/src/wallet_connect/request_handler/sui.rs @@ -7,7 +7,7 @@ use serde_json::Value; pub struct SuiRequestHandler; impl ChainRequestHandler for SuiRequestHandler { - fn parse_sign_message(_chain: Chain, params: Value) -> Result { + fn parse_sign_message(_chain: Chain, params: Value, _domain: &str) -> Result { let message = params.get("message").and_then(|v| v.as_str()).ok_or("Missing message parameter")?; Ok(WalletConnectAction::SignMessage { @@ -53,7 +53,7 @@ mod tests { #[test] fn test_parse_sign_message() { let params = serde_json::from_str(r#"{"message":"Hello Sui"}"#).unwrap(); - let action = SuiRequestHandler::parse_sign_message(Chain::Sui, params).unwrap(); + let action = SuiRequestHandler::parse_sign_message(Chain::Sui, params, "example.com").unwrap(); match action { WalletConnectAction::SignMessage { chain, sign_type, data } => { assert_eq!(chain, Chain::Sui); diff --git a/gemstone/src/wallet_connect/request_handler/ton.rs b/gemstone/src/wallet_connect/request_handler/ton.rs index 27de5bb78..ca8f4e506 100644 --- a/gemstone/src/wallet_connect/request_handler/ton.rs +++ b/gemstone/src/wallet_connect/request_handler/ton.rs @@ -3,14 +3,21 @@ use crate::wallet_connect::actions::{WalletConnectAction, WalletConnectTransacti use crate::wallet_connect::handler_traits::ChainRequestHandler; use primitives::{Chain, TransferDataOutputType}; use serde_json::Value; +use signer::TonSignMessageData; pub struct TonRequestHandler; +fn extract_host(url: &str) -> String { + url::Url::parse(url).map(|u| u.host_str().unwrap_or(url).to_string()).unwrap_or_else(|_| url.to_string()) +} + impl ChainRequestHandler for TonRequestHandler { - fn parse_sign_message(_chain: Chain, params: Value) -> Result { + fn parse_sign_message(_chain: Chain, params: Value, domain: &str) -> Result { let params_array = params.as_array().ok_or("Invalid params format")?; - let payload = params_array.first().ok_or("Missing payload parameter")?; - let data = payload.to_string(); + let payload = params_array.first().ok_or("Missing payload parameter")?.clone(); + let host = extract_host(domain); + let ton_data = TonSignMessageData::new(payload, host); + let data = String::from_utf8(ton_data.to_bytes()).map_err(|e| format!("Failed to encode TonSignMessageData: {}", e))?; Ok(WalletConnectAction::SignMessage { chain: Chain::Ton, sign_type: SignDigestType::TonPersonal, @@ -48,13 +55,29 @@ mod tests { #[test] fn test_parse_sign_message() { let params = serde_json::from_str(r#"[{"type":"text","text":"Hello TON"}]"#).unwrap(); - let action = TonRequestHandler::parse_sign_message(Chain::Ton, params).unwrap(); + let action = TonRequestHandler::parse_sign_message(Chain::Ton, params, "https://react-app.walletconnect.com").unwrap(); let WalletConnectAction::SignMessage { chain, sign_type, data } = action else { panic!("Expected SignMessage action") }; assert_eq!(chain, Chain::Ton); assert_eq!(sign_type, SignDigestType::TonPersonal); - assert_eq!(data, r#"{"type":"text","text":"Hello TON"}"#); + + let parsed: TonSignMessageData = serde_json::from_str(&data).unwrap(); + assert_eq!(parsed.domain, "react-app.walletconnect.com"); + assert_eq!(parsed.payload["type"], "text"); + assert_eq!(parsed.payload["text"], "Hello TON"); + } + + #[test] + fn test_parse_sign_message_extracts_host() { + let params = serde_json::from_str(r#"[{"type":"text","text":"Test"}]"#).unwrap(); + let action = TonRequestHandler::parse_sign_message(Chain::Ton, params, "https://example.com/path?query=1").unwrap(); + let WalletConnectAction::SignMessage { data, .. } = action else { + panic!("Expected SignMessage action") + }; + + let parsed: TonSignMessageData = serde_json::from_str(&data).unwrap(); + assert_eq!(parsed.domain, "example.com"); } #[test] @@ -82,7 +105,9 @@ mod tests { panic!("Expected Ton transaction type with EncodedTransaction output") }; let parsed_data: serde_json::Value = serde_json::from_str(&data).expect("Data should be valid JSON"); - assert!(parsed_data.get("messages").is_some()); + assert_eq!(parsed_data["valid_until"], 1234567890); + assert_eq!(parsed_data["messages"][0]["address"], "0:1234567890abcdef"); + assert_eq!(parsed_data["messages"][0]["amount"], "1000000000"); } #[test] From eaec9da054bb7d1eb83e39ea202ff789ed2d240d Mon Sep 17 00:00:00 2001 From: Radmir <52320354+DRadmir@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:57:00 +0500 Subject: [PATCH 07/11] Refactor TON sign data and improve error handling - Refactor TonSignDataPayload to serde tagged enum, removing manual parsing - Remove TonSignDataType enum and strum dependency from signer - Change MessageSigner::hash() to return Result for safety (prevents signing empty data on errors) - Add From for GemstoneError - Simplify error handling using ? operator throughout - Remove redundant TON-specific tests --- Cargo.lock | 1 - .../src/wallet_connect_namespace.rs | 10 -- crates/signer/Cargo.toml | 1 - crates/signer/src/lib.rs | 6 +- crates/signer/src/ton.rs | 106 +++++------------- gemstone/src/lib.rs | 6 + gemstone/src/message/signer.rs | 61 +++++----- gemstone/src/wallet_connect/mod.rs | 12 +- .../src/wallet_connect/response_handler.rs | 6 +- 9 files changed, 66 insertions(+), 143 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc5fb34c6..a22f8146a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6557,7 +6557,6 @@ dependencies = [ "k256", "serde", "serde_json", - "strum", "sui-sdk-types", "zeroize", ] diff --git a/crates/primitives/src/wallet_connect_namespace.rs b/crates/primitives/src/wallet_connect_namespace.rs index 06c96ef0a..b1365bbac 100644 --- a/crates/primitives/src/wallet_connect_namespace.rs +++ b/crates/primitives/src/wallet_connect_namespace.rs @@ -145,14 +145,4 @@ mod tests { assert!(WalletConnectCAIP2::resolve_chain(None).is_err()); assert!(WalletConnectCAIP2::resolve_chain(Some("unknown:chain".to_string())).is_err()); } - - #[test] - fn test_get_namespace_ton() { - assert_eq!(WalletConnectCAIP2::get_namespace(Chain::Ton), Some("ton".to_string())); - } - - #[test] - fn test_get_reference_ton() { - assert_eq!(WalletConnectCAIP2::get_reference(Chain::Ton), Some("-239".to_string())); - } } diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index 24291230b..fe3728da2 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -14,6 +14,5 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } gem_hash = { path = "../gem_hash" } zeroize = { version = "1.8.2" } -strum = { workspace = true } [dev-dependencies] diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 2c085bbab..e3006514b 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -18,9 +18,7 @@ use crate::sui::assemble_signature; pub use eip712::hash_typed_data as hash_eip712; pub use error::SignerError; pub use sui::SUI_PERSONAL_MESSAGE_SIGNATURE_LEN; -pub use ton::{TonSignDataPayload, TonSignDataResponse, TonSignDataType, TonSignMessageData}; - -use crate::ton::TonSignMessageData as TonSignMessageDataInternal; +pub use ton::{TonSignDataPayload, TonSignDataResponse, TonSignMessageData}; #[derive(Debug, Default)] pub struct Signer; @@ -64,7 +62,7 @@ impl Signer { } pub fn sign_ton_personal(data: &[u8], private_key: &[u8]) -> Result<(Vec, Vec), SignerError> { - let ton_data = TonSignMessageDataInternal::from_bytes(data)?; + let ton_data = TonSignMessageData::from_bytes(data)?; let payload = ton_data.get_payload()?; let digest = payload.hash(); diff --git a/crates/signer/src/ton.rs b/crates/signer/src/ton.rs index 361de7e63..92c401491 100644 --- a/crates/signer/src/ton.rs +++ b/crates/signer/src/ton.rs @@ -1,77 +1,41 @@ -use std::str::FromStr; - use serde::{Deserialize, Serialize}; -use strum::{AsRefStr, EnumString}; use crate::error::SignerError; -#[derive(Clone, Debug, PartialEq, AsRefStr, EnumString)] -#[strum(serialize_all = "lowercase")] -pub enum TonSignDataType { - Text, - Binary, - Cell, -} - -impl TonSignDataType { - pub fn data_field(&self) -> &'static str { - match self { - TonSignDataType::Text => "text", - TonSignDataType::Binary => "bytes", - TonSignDataType::Cell => "cell", - } - } -} - -#[derive(Deserialize)] -struct TonSignDataPayloadRaw { - #[serde(rename = "type")] - payload_type: String, - text: Option, - bytes: Option, - cell: Option, -} - -pub struct TonSignDataPayload { - pub payload_type: TonSignDataType, - pub data: String, +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum TonSignDataPayload { + Text { text: String }, + Binary { bytes: String }, + Cell { cell: String }, } impl TonSignDataPayload { pub fn parse(json: &str) -> Result { - let raw: TonSignDataPayloadRaw = serde_json::from_str(json)?; - - let payload_type = TonSignDataType::from_str(&raw.payload_type).map_err(|_| SignerError::new(format!("Unknown payload type: {}", raw.payload_type)))?; - - let data = match payload_type { - TonSignDataType::Text => raw.text.ok_or("Missing text field")?, - TonSignDataType::Binary => raw.bytes.ok_or("Missing bytes field")?, - TonSignDataType::Cell => raw.cell.ok_or("Missing cell field")?, - }; - - Ok(Self { payload_type, data }) + serde_json::from_str(json).map_err(SignerError::from) } - pub fn hash(&self) -> Vec { - self.data.as_bytes().to_vec() + pub fn data(&self) -> &str { + match self { + Self::Text { text } => text, + Self::Binary { bytes } => bytes, + Self::Cell { cell } => cell, + } } - pub fn to_json(&self) -> serde_json::Value { - serde_json::json!({ - "type": self.payload_type.as_ref(), - self.payload_type.data_field(): self.data, - }) + pub fn hash(&self) -> Vec { + self.data().as_bytes().to_vec() } } #[derive(Serialize)] +#[serde(rename_all = "camelCase")] pub struct TonSignDataResponse { signature: String, - #[serde(rename = "publicKey")] public_key: String, timestamp: u64, domain: String, - payload: serde_json::Value, + payload: TonSignDataPayload, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -100,7 +64,7 @@ impl TonSignMessageData { } impl TonSignDataResponse { - pub fn new(signature: String, public_key: String, timestamp: u64, domain: String, payload: serde_json::Value) -> Self { + pub fn new(signature: String, public_key: String, timestamp: u64, domain: String, payload: TonSignDataPayload) -> Self { Self { signature, public_key, @@ -124,8 +88,8 @@ mod tests { let json = r#"{"type":"text","text":"Hello TON"}"#; let parsed = TonSignDataPayload::parse(json).unwrap(); - assert_eq!(parsed.payload_type, TonSignDataType::Text); - assert_eq!(parsed.data, "Hello TON"); + assert_eq!(parsed, TonSignDataPayload::Text { text: "Hello TON".to_string() }); + assert_eq!(parsed.data(), "Hello TON"); assert_eq!(parsed.hash(), b"Hello TON".to_vec()); } @@ -134,8 +98,7 @@ mod tests { let json = r#"{"type":"binary","bytes":"SGVsbG8="}"#; let parsed = TonSignDataPayload::parse(json).unwrap(); - assert_eq!(parsed.payload_type, TonSignDataType::Binary); - assert_eq!(parsed.data, "SGVsbG8="); + assert_eq!(parsed, TonSignDataPayload::Binary { bytes: "SGVsbG8=".to_string() }); } #[test] @@ -143,35 +106,19 @@ mod tests { let json = r#"{"type":"cell","cell":"te6c"}"#; let parsed = TonSignDataPayload::parse(json).unwrap(); - assert_eq!(parsed.payload_type, TonSignDataType::Cell); - assert_eq!(parsed.data, "te6c"); - } - - #[test] - fn test_payload_to_json() { - let payload = TonSignDataPayload { - payload_type: TonSignDataType::Text, - data: "Hello TON".to_string(), - }; - - let json = payload.to_json(); - assert_eq!(json["type"], "text"); - assert_eq!(json["text"], "Hello TON"); + assert_eq!(parsed, TonSignDataPayload::Cell { cell: "te6c".to_string() }); } #[test] fn test_response_to_json() { - let payload = TonSignDataPayload { - payload_type: TonSignDataType::Text, - data: "Hello TON".to_string(), - }; + let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; let response = TonSignDataResponse::new( "c2lnbmF0dXJl".to_string(), "cHVibGljS2V5".to_string(), 1234567890, "example.com".to_string(), - payload.to_json(), + payload, ); let json = response.to_json().unwrap(); @@ -186,7 +133,7 @@ mod tests { } #[test] - fn test_ton_sign_message_data_roundtrip() { + fn test_ton_sign_message_data() { let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); let data = TonSignMessageData::new(payload.clone(), "example.com".to_string()); @@ -203,7 +150,6 @@ mod tests { let data = TonSignMessageData::new(payload, "example.com".to_string()); let parsed_payload = data.get_payload().unwrap(); - assert_eq!(parsed_payload.payload_type, TonSignDataType::Text); - assert_eq!(parsed_payload.data, "Hello TON"); + assert_eq!(parsed_payload, TonSignDataPayload::Text { text: "Hello TON".to_string() }); } } diff --git a/gemstone/src/lib.rs b/gemstone/src/lib.rs index fe8c7a284..3fef0361a 100644 --- a/gemstone/src/lib.rs +++ b/gemstone/src/lib.rs @@ -100,3 +100,9 @@ impl From for GemstoneError { Self::AnyError { msg: error.to_string() } } } + +impl From for GemstoneError { + fn from(error: std::string::FromUtf8Error) -> Self { + Self::AnyError { msg: error.to_string() } + } +} diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index 197278d11..f7737b54a 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -46,17 +46,17 @@ impl MessageSigner { Ok(MessagePreview::Text(preview)) } SignDigestType::TonPersonal => { - let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8"))?; + let string = String::from_utf8(self.message.data.clone())?; let Ok(ton_data) = TonSignMessageData::from_bytes(string.as_bytes()) else { return Ok(MessagePreview::Text(string)); }; let Ok(payload) = ton_data.get_payload() else { return Ok(MessagePreview::Text(string)); }; - Ok(MessagePreview::Text(payload.data)) + Ok(MessagePreview::Text(payload.data().to_string())) } SignDigestType::Eip712 => { - let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8 string for EIP712"))?; + let string = String::from_utf8(self.message.data.clone())?; if string.is_empty() { return Err(GemstoneError::from("Empty EIP712 message string")); } @@ -98,34 +98,27 @@ impl MessageSigner { } } - pub fn hash(&self) -> Vec { + pub fn hash(&self) -> Result, GemstoneError> { match &self.message.sign_type { SignDigestType::SuiPersonal => { let message = PersonalMessage(Cow::Borrowed(&self.message.data)); - message.signing_digest().to_vec() + Ok(message.signing_digest().to_vec()) } SignDigestType::TonPersonal => { - let Ok(string) = String::from_utf8(self.message.data.clone()) else { - return Vec::new(); - }; - let Ok(ton_data) = TonSignMessageData::from_bytes(string.as_bytes()) else { - return Vec::new(); - }; - let Ok(payload) = ton_data.get_payload() else { - return Vec::new(); - }; - payload.hash() + let string = String::from_utf8(self.message.data.clone())?; + let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; + let payload = ton_data.get_payload()?; + Ok(payload.hash()) + } + SignDigestType::Eip191 | SignDigestType::Siwe => Ok(eip191_hash_message(&self.message.data).to_vec()), + SignDigestType::Eip712 => { + let json = String::from_utf8(self.message.data.clone())?; + let digest = hash_eip712(&json)?; + Ok(digest.to_vec()) } - SignDigestType::Eip191 | SignDigestType::Siwe => eip191_hash_message(&self.message.data).to_vec(), - SignDigestType::Eip712 => match std::str::from_utf8(&self.message.data) { - Ok(json) => hash_eip712(json).map(|digest| digest.to_vec()).unwrap_or_default(), - Err(_) => Vec::new(), - }, SignDigestType::Base58 => { - if let Ok(decoded) = bs58::decode(&self.message.data).into_vec() { - return decoded; - } - Vec::new() + let decoded = bs58::decode(&self.message.data).into_vec().map_err(|e| GemstoneError::from(e.to_string()))?; + Ok(decoded) } } } @@ -148,22 +141,22 @@ impl MessageSigner { } pub fn get_ton_result(&self, signature: &[u8], public_key: &[u8]) -> Result { - let string = String::from_utf8(self.message.data.clone()).map_err(|_| GemstoneError::from("Invalid UTF-8"))?; - let ton_data = TonSignMessageData::from_bytes(string.as_bytes()).map_err(|e| GemstoneError::from(e.to_string()))?; - let payload = ton_data.get_payload().map_err(|e| GemstoneError::from(e.to_string()))?; + let string = String::from_utf8(self.message.data.clone())?; + let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; + let payload = ton_data.get_payload()?; let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); - let response = TonSignDataResponse::new(BASE64.encode(signature), BASE64.encode(public_key), timestamp, ton_data.domain, payload.to_json()); + let response = TonSignDataResponse::new(BASE64.encode(signature), BASE64.encode(public_key), timestamp, ton_data.domain, payload); - response.to_json().map_err(|e| GemstoneError::from(e.to_string())) + Ok(response.to_json()?) } pub fn sign(&self, private_key: Vec) -> Result { let private_key = Zeroizing::new(private_key); - let hash = self.hash(); + let hash = self.hash()?; match &self.message.sign_type { SignDigestType::SuiPersonal => Signer::sign_sui_digest(&hash, &private_key).map_err(GemstoneError::from), SignDigestType::TonPersonal => { @@ -206,7 +199,7 @@ mod tests { _ => panic!("Unexpected preview result"), } - let hash = decoder.hash(); + let hash = decoder.hash().unwrap(); assert_eq!(encode_prefixed(&hash), "0xd9eba16ed0ecae432b71fe008c98cc872bb4cc214d3220a36f365326cf807d68"); } @@ -292,7 +285,7 @@ mod tests { data: data.clone(), }); - let hash = decoder.hash(); + let hash = decoder.hash().unwrap(); let expected_hash = PersonalMessage(Cow::Owned(data)).signing_digest().to_vec(); assert_eq!(hash, expected_hash); @@ -322,7 +315,7 @@ mod tests { Ok(MessagePreview::Text(preview)) => assert_eq!(preview, "This is an example message to be signed - 1747125759060"), _ => panic!("Unexpected preview result for base58"), } - let hash = decoder.hash(); + let hash = decoder.hash().unwrap(); assert_eq!( hex::encode(&hash), @@ -415,7 +408,7 @@ mod tests { data: json_str.as_bytes().to_vec(), }); - let digest = decoder.hash(); + let digest = decoder.hash().unwrap(); assert_eq!(hex::encode(digest), "480af9fd3cdc70c2f8a521388be13620d16a0f643d9cffdfbb65cd019cc27537"); } diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index ca7889cbe..8570d9578 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -3,7 +3,6 @@ use std::time::{SystemTime, UNIX_EPOCH}; use hex::FromHex; use primitives::{Chain, WCEthereumTransaction, WalletConnectRequest, WalletConnectionVerificationStatus}; -use signer::TonSignDataType; fn current_timestamp() -> i64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0) @@ -218,15 +217,8 @@ impl WalletConnect { gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } SignDigestType::TonPersonal => { - let ton_data = signer::TonSignMessageData::from_bytes(data.as_bytes()).map_err(|e| crate::GemstoneError::AnyError { - msg: format!("Invalid TonSignMessageData: {}", e), - })?; - let payload_type = ton_data.payload.get("type").and_then(|v| v.as_str()).ok_or_else(|| crate::GemstoneError::AnyError { - msg: "Missing type field in payload".to_string(), - })?; - TonSignDataType::from_str(payload_type).map_err(|_| crate::GemstoneError::AnyError { - msg: format!("Unsupported payload type: {}", payload_type), - })?; + let ton_data = signer::TonSignMessageData::from_bytes(data.as_bytes())?; + ton_data.get_payload()?; Ok(()) } SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe => Ok(()), diff --git a/gemstone/src/wallet_connect/response_handler.rs b/gemstone/src/wallet_connect/response_handler.rs index e884df76c..95a26fe5c 100644 --- a/gemstone/src/wallet_connect/response_handler.rs +++ b/gemstone/src/wallet_connect/response_handler.rs @@ -168,11 +168,11 @@ mod tests { #[test] fn test_encode_sign_message_ton() { - let ton_json = r#"{"signature":"tonsig123","timestamp":1700000000}"#.to_string(); - let result = WalletConnectResponseHandler::encode_sign_message(ChainType::Ton, ton_json.clone()); + let payload_json = r#"{"signature":"tonsig123","timestamp":1700000000}"#.to_string(); + let result = WalletConnectResponseHandler::encode_sign_message(ChainType::Ton, payload_json.clone()); match result { WalletConnectResponseType::Object { json } => { - assert_eq!(json, ton_json); + assert_eq!(json, payload_json); } _ => panic!("Expected Object response for Ton"), } From fedebf5df0eecf2385b35d25ce4fdafe2635bdd3 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:39:33 +0900 Subject: [PATCH 08/11] move sui/ton message signing logic from signer to each crate --- Cargo.lock | 7 +- crates/gem_sui/Cargo.toml | 5 +- .../src/{signer.rs => signer/chain_signer.rs} | 7 +- crates/gem_sui/src/signer/mod.rs | 5 + crates/gem_sui/src/signer/signature.rs | 98 ++++++++++++++++++ crates/gem_ton/Cargo.toml | 5 + crates/gem_ton/src/lib.rs | 3 + crates/gem_ton/src/signer/chain_signer.rs | 27 +++++ crates/gem_ton/src/signer/mod.rs | 7 ++ crates/gem_ton/src/signer/signature.rs | 54 ++++++++++ .../ton.rs => gem_ton/src/signer/types.rs} | 3 +- crates/signer/Cargo.toml | 2 - crates/signer/src/ed25519.rs | 2 +- crates/signer/src/lib.rs | 99 +------------------ crates/signer/src/sui.rs | 25 ----- gemstone/Cargo.toml | 2 +- gemstone/src/message/signer.rs | 14 ++- gemstone/src/wallet_connect/mod.rs | 13 ++- .../src/wallet_connect/request_handler/ton.rs | 6 +- .../GemTest/GemTest.xcodeproj/project.pbxproj | 4 +- .../GemTest/GemTestTests/GemTestTests.swift | 2 +- 21 files changed, 241 insertions(+), 149 deletions(-) rename crates/gem_sui/src/{signer.rs => signer/chain_signer.rs} (91%) create mode 100644 crates/gem_sui/src/signer/mod.rs create mode 100644 crates/gem_sui/src/signer/signature.rs create mode 100644 crates/gem_ton/src/signer/chain_signer.rs create mode 100644 crates/gem_ton/src/signer/mod.rs create mode 100644 crates/gem_ton/src/signer/signature.rs rename crates/{signer/src/ton.rs => gem_ton/src/signer/types.rs} (99%) delete mode 100644 crates/signer/src/sui.rs diff --git a/Cargo.lock b/Cargo.lock index a22f8146a..d25a595b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3040,6 +3040,7 @@ dependencies = [ "chain_primitives", "chain_traits", "chrono", + "ed25519-dalek", "futures", "gem_client", "gem_jsonrpc", @@ -3052,10 +3053,10 @@ dependencies = [ "serde_json", "serde_serializers", "settings", - "signer", "sui-sdk-types", "sui-transaction-builder", "tokio", + "zeroize", ] [[package]] @@ -3067,6 +3068,7 @@ dependencies = [ "chain_traits", "chrono", "crc", + "ed25519-dalek", "futures", "gem_client", "hex", @@ -3078,6 +3080,7 @@ dependencies = [ "serde_serializers", "settings", "tokio", + "zeroize", ] [[package]] @@ -6550,14 +6553,12 @@ name = "signer" version = "1.0.0" dependencies = [ "alloy-primitives", - "base64 0.22.1", "ed25519-dalek", "gem_hash", "hex", "k256", "serde", "serde_json", - "sui-sdk-types", "zeroize", ] diff --git a/crates/gem_sui/Cargo.toml b/crates/gem_sui/Cargo.toml index be08bd37f..e6ad99e03 100644 --- a/crates/gem_sui/Cargo.toml +++ b/crates/gem_sui/Cargo.toml @@ -11,7 +11,7 @@ rpc = [ "dep:gem_client", "dep:chain_traits", ] -signer = ["dep:signer"] +signer = ["dep:ed25519-dalek", "dep:zeroize"] reqwest = ["gem_jsonrpc/reqwest"] chain_integration_tests = ["rpc", "reqwest", "settings/testkit"] @@ -31,7 +31,8 @@ num-traits = { workspace = true } gem_jsonrpc = { path = "../gem_jsonrpc" } futures = { workspace = true } -signer = { path = "../signer", optional = true } +ed25519-dalek = { version = "2.1.1", default-features = false, features = ["std", "zeroize"], optional = true } +zeroize = { version = "1.8.2", optional = true } # Optional dependencies for rpc feature num-bigint = { workspace = true, features = ["serde"] } diff --git a/crates/gem_sui/src/signer.rs b/crates/gem_sui/src/signer/chain_signer.rs similarity index 91% rename from crates/gem_sui/src/signer.rs rename to crates/gem_sui/src/signer/chain_signer.rs index 6daf45a69..efde6824a 100644 --- a/crates/gem_sui/src/signer.rs +++ b/crates/gem_sui/src/signer/chain_signer.rs @@ -1,7 +1,8 @@ -use ::signer::Signer; use hex::decode; use primitives::{ChainSigner, SignerError, TransactionInputType, TransactionLoadInput, stake_type::StakeType}; +use super::signature::{sign_digest, sign_personal_message}; + #[derive(Default)] pub struct SuiChainSigner; @@ -47,7 +48,7 @@ impl ChainSigner for SuiChainSigner { } fn sign_message(&self, message: &[u8], private_key: &[u8]) -> Result { - Signer::sign_sui_personal_message(message, private_key).map_err(|err| SignerError::InvalidInput(err.to_string())) + sign_personal_message(message, private_key) } } @@ -63,7 +64,7 @@ pub fn sign_message_bytes(message: &str, private_key: &[u8]) -> Result Result { + let personal_message = PersonalMessage(Cow::Borrowed(message)); + let digest = personal_message.signing_digest(); + sign_digest(digest.as_ref(), private_key) +} + +pub fn sign_digest(digest: &[u8], private_key: &[u8]) -> Result { + let private_key = Zeroizing::new(private_key.to_vec()); + let signing_key = signing_key_from_bytes(&private_key)?; + let signature = signing_key.sign(digest); + let signature_bytes = signature.to_bytes(); + let public_key_bytes = signing_key.verifying_key().to_bytes(); + + assemble_signature(&signature_bytes, &public_key_bytes) +} + +fn signing_key_from_bytes(private_key: &[u8]) -> Result { + let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key + .try_into() + .map_err(|_| SignerError::InvalidInput("Invalid Ed25519 private key length".to_string()))?; + Ok(ed25519_dalek::SigningKey::from_bytes(&key_bytes)) +} + +fn assemble_signature(signature: &[u8], public_key: &[u8]) -> Result { + let signature_bytes: [u8; Ed25519Signature::LENGTH] = signature + .try_into() + .map_err(|_| SignerError::InvalidInput(format!("Expected {} byte ed25519 signature, got {}", Ed25519Signature::LENGTH, signature.len())))?; + let public_key_bytes: [u8; Ed25519PublicKey::LENGTH] = public_key.try_into().map_err(|_| { + SignerError::InvalidInput(format!( + "Expected {} byte ed25519 public key, got {}", + Ed25519PublicKey::LENGTH, + public_key.len() + )) + })?; + + let sui_signature = SimpleSignature::Ed25519 { + signature: Ed25519Signature::new(signature_bytes), + public_key: Ed25519PublicKey::new(public_key_bytes), + }; + + Ok(UserSignature::Simple(sui_signature).to_base64()) +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::Engine as _; + use base64::engine::general_purpose::STANDARD; + use ed25519_dalek::{Signature, SigningKey, Verifier}; + + #[test] + fn test_sui_sign_personal_message() { + let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").expect("valid hex"); + let message = b"Hello, world!".to_vec(); + + let signature_base64 = sign_personal_message(&message, &private_key).expect("signing succeeds"); + + let signature_bytes = STANDARD.decode(&signature_base64).expect("valid base64"); + assert_eq!(signature_bytes.len(), SUI_PERSONAL_MESSAGE_SIGNATURE_LEN, "signature layout length"); + assert_eq!(signature_bytes[0], 0x00, "expected Ed25519 flag prefix"); + + let signature = &signature_bytes[1..65]; + let public_key_bytes = &signature_bytes[65..]; + + let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key.clone().try_into().expect("32 byte secret key"); + let signing_key = SigningKey::from_bytes(&key_bytes); + assert_eq!(public_key_bytes, signing_key.verifying_key().as_bytes(), "public key suffix matches secret key"); + + let personal_message = PersonalMessage(Cow::Borrowed(message.as_slice())); + let digest = personal_message.signing_digest(); + let signature = Signature::from_bytes(signature.try_into().expect("64 byte signature")); + + signing_key + .verifying_key() + .verify(digest.as_ref(), &signature) + .expect("signature verifies against digest"); + + let expected_base64 = + "ALmKZNcvdmYgYloqKMAq7eSw5neV1mSEKfZProHEh8Ddw+6aJvLpuViFqZCHqwKdCqtzN8a+7jIDQSxbvmt04QDTaUUhl8KlZIHl4tPovwPeI0n2emMVGVaCIgjCM0re4g=="; + assert_eq!(signature_base64, expected_base64); + } + + #[test] + fn ed25519_signing_key_rejects_invalid_length() { + let result = signing_key_from_bytes(&[0u8; 16]); + assert!(result.is_err()); + } +} diff --git a/crates/gem_ton/Cargo.toml b/crates/gem_ton/Cargo.toml index 5ab8d3f6b..bb27e9d42 100644 --- a/crates/gem_ton/Cargo.toml +++ b/crates/gem_ton/Cargo.toml @@ -11,6 +11,7 @@ rpc = [ "dep:chain_traits", "dep:futures", ] +signer = ["dep:ed25519-dalek", "dep:zeroize"] reqwest = ["gem_client/reqwest"] chain_integration_tests = ["rpc", "reqwest", "settings/testkit"] @@ -32,6 +33,10 @@ gem_client = { path = "../gem_client", optional = true } chain_traits = { path = "../chain_traits", optional = true } futures = { workspace = true, optional = true } +# Optional signer dependencies +ed25519-dalek = { version = "2.1.1", default-features = false, features = ["std", "zeroize"], optional = true } +zeroize = { version = "1.8.2", optional = true } + [dev-dependencies] tokio = { workspace = true, features = ["macros", "rt"] } reqwest = { workspace = true } diff --git a/crates/gem_ton/src/lib.rs b/crates/gem_ton/src/lib.rs index 62c0ef5f6..1b6298835 100644 --- a/crates/gem_ton/src/lib.rs +++ b/crates/gem_ton/src/lib.rs @@ -4,6 +4,9 @@ pub mod rpc; #[cfg(feature = "rpc")] pub mod provider; +#[cfg(feature = "signer")] +pub mod signer; + pub mod address; pub mod constants; pub mod models; diff --git a/crates/gem_ton/src/signer/chain_signer.rs b/crates/gem_ton/src/signer/chain_signer.rs new file mode 100644 index 000000000..a240f88ce --- /dev/null +++ b/crates/gem_ton/src/signer/chain_signer.rs @@ -0,0 +1,27 @@ +use primitives::{ChainSigner, SignerError, TransactionLoadInput}; + +use super::signature::sign_personal; + +#[derive(Default)] +pub struct TonChainSigner; + +impl ChainSigner for TonChainSigner { + fn sign_message(&self, message: &[u8], private_key: &[u8]) -> Result { + let (signature, _public_key) = sign_personal(message, private_key)?; + Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature)) + } + + fn sign_transfer(&self, input: &TransactionLoadInput, private_key: &[u8]) -> Result { + self.sign_from_metadata(input, private_key) + } + + fn sign_token_transfer(&self, input: &TransactionLoadInput, private_key: &[u8]) -> Result { + self.sign_from_metadata(input, private_key) + } +} + +impl TonChainSigner { + fn sign_from_metadata(&self, _input: &TransactionLoadInput, _private_key: &[u8]) -> Result { + todo!("TON transaction signing not yet implemented in chain signer") + } +} diff --git a/crates/gem_ton/src/signer/mod.rs b/crates/gem_ton/src/signer/mod.rs new file mode 100644 index 000000000..e2cea077a --- /dev/null +++ b/crates/gem_ton/src/signer/mod.rs @@ -0,0 +1,7 @@ +mod chain_signer; +mod signature; +mod types; + +pub use chain_signer::TonChainSigner; +pub use signature::sign_personal; +pub use types::{TonSignDataPayload, TonSignDataResponse, TonSignMessageData}; diff --git a/crates/gem_ton/src/signer/signature.rs b/crates/gem_ton/src/signer/signature.rs new file mode 100644 index 000000000..4a1ee54f7 --- /dev/null +++ b/crates/gem_ton/src/signer/signature.rs @@ -0,0 +1,54 @@ +use ed25519_dalek::Signer as DalekSigner; +use primitives::SignerError; +use zeroize::Zeroizing; + +use super::types::TonSignMessageData; + +pub fn sign_personal(data: &[u8], private_key: &[u8]) -> Result<(Vec, Vec), SignerError> { + let ton_data = TonSignMessageData::from_bytes(data)?; + let payload = ton_data.get_payload()?; + let digest = payload.hash(); + + let private_key = Zeroizing::new(private_key.to_vec()); + let signing_key = signing_key_from_bytes(&private_key)?; + let signature = signing_key.sign(digest.as_slice()); + let signature_bytes = signature.to_bytes().to_vec(); + let public_key_bytes = signing_key.verifying_key().to_bytes().to_vec(); + + Ok((signature_bytes, public_key_bytes)) +} + +fn signing_key_from_bytes(private_key: &[u8]) -> Result { + let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key + .try_into() + .map_err(|_| SignerError::InvalidInput("Invalid Ed25519 private key length".to_string()))?; + Ok(ed25519_dalek::SigningKey::from_bytes(&key_bytes)) +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::{Signature, SigningKey, Verifier}; + + #[test] + fn test_sign_ton_personal() { + let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); + let data = ton_data.to_bytes(); + + let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").expect("valid hex"); + + let (signature, public_key) = sign_personal(&data, &private_key).expect("signing succeeds"); + + assert_eq!(signature.len(), 64, "Ed25519 signature should be 64 bytes"); + assert_eq!(public_key.len(), 32, "Ed25519 public key should be 32 bytes"); + + let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key.try_into().expect("32 byte secret key"); + let signing_key = SigningKey::from_bytes(&key_bytes); + assert_eq!(public_key, signing_key.verifying_key().as_bytes(), "public key should match"); + + let signature = Signature::from_bytes(signature.as_slice().try_into().expect("64 byte signature")); + let digest = b"Hello TON"; + signing_key.verifying_key().verify(digest, &signature).expect("signature verifies"); + } +} diff --git a/crates/signer/src/ton.rs b/crates/gem_ton/src/signer/types.rs similarity index 99% rename from crates/signer/src/ton.rs rename to crates/gem_ton/src/signer/types.rs index 92c401491..81719165e 100644 --- a/crates/signer/src/ton.rs +++ b/crates/gem_ton/src/signer/types.rs @@ -1,7 +1,6 @@ +use primitives::SignerError; use serde::{Deserialize, Serialize}; -use crate::error::SignerError; - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "lowercase")] pub enum TonSignDataPayload { diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index fe3728da2..7401cbb7e 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -6,8 +6,6 @@ edition = { workspace = true } [dependencies] ed25519-dalek = { version = "2.1.1", default-features = false, features = ["std", "zeroize"] } k256 = { workspace = true } -sui-types = { workspace = true, features = ["hash"] } -base64 = { workspace = true } hex = { workspace = true } alloy-primitives = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/signer/src/ed25519.rs b/crates/signer/src/ed25519.rs index 494b4616b..f524d3285 100644 --- a/crates/signer/src/ed25519.rs +++ b/crates/signer/src/ed25519.rs @@ -2,7 +2,7 @@ use ed25519_dalek::{Signer as DalekSigner, SigningKey}; use crate::error::SignerError; -pub(crate) fn signing_key_from_bytes(private_key: &[u8]) -> Result { +pub fn signing_key_from_bytes(private_key: &[u8]) -> Result { let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key.try_into().map_err(|_| SignerError::new("Invalid Ed25519 private key length"))?; Ok(SigningKey::from_bytes(&key_bytes)) } diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index e3006514b..412e69da0 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -2,23 +2,15 @@ mod ed25519; mod eip712; mod error; mod secp256k1; -mod sui; -mod ton; -use std::borrow::Cow; - -use ed25519_dalek::Signer as DalekSigner; -use sui_types::PersonalMessage; use zeroize::Zeroizing; -use crate::ed25519::{sign_digest as sign_ed25519_digest, signing_key_from_bytes}; +use crate::ed25519::sign_digest as sign_ed25519_digest; use crate::secp256k1::sign_digest as sign_secp256k1_digest; -use crate::sui::assemble_signature; +pub use ed25519::signing_key_from_bytes; pub use eip712::hash_typed_data as hash_eip712; pub use error::SignerError; -pub use sui::SUI_PERSONAL_MESSAGE_SIGNATURE_LEN; -pub use ton::{TonSignDataPayload, TonSignDataResponse, TonSignMessageData}; #[derive(Debug, Default)] pub struct Signer; @@ -30,22 +22,6 @@ pub enum SignatureScheme { } impl Signer { - pub fn sign_sui_personal_message(message: &[u8], private_key: &[u8]) -> Result { - let personal_message = PersonalMessage(Cow::Borrowed(message)); - let digest = personal_message.signing_digest(); - Self::sign_sui_digest(digest.as_ref(), private_key) - } - - pub fn sign_sui_digest(digest: &[u8], private_key: &[u8]) -> Result { - let private_key = Zeroizing::new(private_key.to_vec()); - let signing_key = signing_key_from_bytes(&private_key)?; - let signature = signing_key.sign(digest); - let signature_bytes = signature.to_bytes(); - let public_key_bytes = signing_key.verifying_key().to_bytes(); - - assemble_signature(&signature_bytes, &public_key_bytes) - } - pub fn sign_digest(scheme: SignatureScheme, digest: Vec, private_key: Vec) -> Result, SignerError> { let private_key = Zeroizing::new(private_key); match scheme { @@ -60,86 +36,15 @@ impl Signer { let signature = Self::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key_vec.to_vec())?; Ok(hex::encode(signature)) } - - pub fn sign_ton_personal(data: &[u8], private_key: &[u8]) -> Result<(Vec, Vec), SignerError> { - let ton_data = TonSignMessageData::from_bytes(data)?; - let payload = ton_data.get_payload()?; - let digest = payload.hash(); - - let private_key = Zeroizing::new(private_key.to_vec()); - let signing_key = signing_key_from_bytes(&private_key)?; - let signature = signing_key.sign(digest.as_slice()); - let signature_bytes = signature.to_bytes().to_vec(); - let public_key_bytes = signing_key.verifying_key().to_bytes().to_vec(); - - Ok((signature_bytes, public_key_bytes)) - } } #[cfg(test)] mod tests { use super::*; - use base64::Engine as _; - use base64::engine::general_purpose::STANDARD; - use ed25519_dalek::{Signature, SigningKey, Verifier}; - - #[test] - fn test_sui_sign_personal_message() { - let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").expect("valid hex"); - let message = b"Hello, world!".to_vec(); - - let signature_base64 = Signer::sign_sui_personal_message(&message, &private_key).expect("signing succeeds"); - - let signature_bytes = STANDARD.decode(&signature_base64).expect("valid base64"); - assert_eq!(signature_bytes.len(), SUI_PERSONAL_MESSAGE_SIGNATURE_LEN, "signature layout length"); - assert_eq!(signature_bytes[0], 0x00, "expected Ed25519 flag prefix"); - - let signature = &signature_bytes[1..65]; - let public_key_bytes = &signature_bytes[65..]; - - let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key.clone().try_into().expect("32 byte secret key"); - let signing_key = SigningKey::from_bytes(&key_bytes); - assert_eq!(public_key_bytes, signing_key.verifying_key().as_bytes(), "public key suffix matches secret key"); - - let personal_message = PersonalMessage(Cow::Borrowed(message.as_slice())); - let digest = personal_message.signing_digest(); - let signature = Signature::from_bytes(signature.try_into().expect("64 byte signature")); - - signing_key - .verifying_key() - .verify(digest.as_ref(), &signature) - .expect("signature verifies against digest"); - - let expected_base64 = - "ALmKZNcvdmYgYloqKMAq7eSw5neV1mSEKfZProHEh8Ddw+6aJvLpuViFqZCHqwKdCqtzN8a+7jIDQSxbvmt04QDTaUUhl8KlZIHl4tPovwPeI0n2emMVGVaCIgjCM0re4g=="; - assert_eq!(signature_base64, expected_base64); - } #[test] fn ed25519_signing_key_rejects_invalid_length() { let result = signing_key_from_bytes(&[0u8; 16]); assert!(result.is_err()); } - - #[test] - fn test_sign_ton_personal() { - let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); - let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); - let data = ton_data.to_bytes(); - - let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").expect("valid hex"); - - let (signature, public_key) = Signer::sign_ton_personal(&data, &private_key).expect("signing succeeds"); - - assert_eq!(signature.len(), 64, "Ed25519 signature should be 64 bytes"); - assert_eq!(public_key.len(), 32, "Ed25519 public key should be 32 bytes"); - - let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key.try_into().expect("32 byte secret key"); - let signing_key = SigningKey::from_bytes(&key_bytes); - assert_eq!(public_key, signing_key.verifying_key().as_bytes(), "public key should match"); - - let signature = Signature::from_bytes(signature.as_slice().try_into().expect("64 byte signature")); - let digest = b"Hello TON"; - signing_key.verifying_key().verify(digest, &signature).expect("signature verifies"); - } } diff --git a/crates/signer/src/sui.rs b/crates/signer/src/sui.rs deleted file mode 100644 index 598b37977..000000000 --- a/crates/signer/src/sui.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::error::SignerError; -use sui_types::{Ed25519PublicKey, Ed25519Signature, SimpleSignature, UserSignature}; - -/// 1-byte flag + 64-byte signature + 32-byte public key. -pub const SUI_PERSONAL_MESSAGE_SIGNATURE_LEN: usize = 1 + Ed25519Signature::LENGTH + Ed25519PublicKey::LENGTH; - -pub(crate) fn assemble_signature(signature: &[u8], public_key: &[u8]) -> Result { - let signature_bytes: [u8; Ed25519Signature::LENGTH] = signature - .try_into() - .map_err(|_| SignerError::new(format!("Expected {} byte ed25519 signature, got {}", Ed25519Signature::LENGTH, signature.len())))?; - let public_key_bytes: [u8; Ed25519PublicKey::LENGTH] = public_key.try_into().map_err(|_| { - SignerError::new(format!( - "Expected {} byte ed25519 public key, got {}", - Ed25519PublicKey::LENGTH, - public_key.len() - )) - })?; - - let sui_signature = SimpleSignature::Ed25519 { - signature: Ed25519Signature::new(signature_bytes), - public_key: Ed25519PublicKey::new(public_key_bytes), - }; - - Ok(UserSignature::Simple(sui_signature).to_base64()) -} diff --git a/gemstone/Cargo.toml b/gemstone/Cargo.toml index 4de97b884..1917293da 100644 --- a/gemstone/Cargo.toml +++ b/gemstone/Cargo.toml @@ -22,7 +22,7 @@ swapper = { path = "../crates/swapper" } primitives = { path = "../crates/primitives" } gem_cosmos = { path = "../crates/gem_cosmos", features = ["rpc"] } gem_solana = { path = "../crates/gem_solana", features = ["rpc"] } -gem_ton = { path = "../crates/gem_ton", features = ["rpc"] } +gem_ton = { path = "../crates/gem_ton", features = ["rpc", "signer"] } gem_tron = { path = "../crates/gem_tron", features = ["rpc"] } gem_evm = { path = "../crates/gem_evm", features = ["rpc"] } gem_sui = { path = "../crates/gem_sui", features = ["rpc", "signer"] } diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index f7737b54a..a11cf48a5 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -4,7 +4,9 @@ use alloy_primitives::{eip191_hash_message, hex::encode_prefixed}; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; use bs58; -use signer::{TonSignDataResponse, TonSignMessageData}; +use gem_sui::signer as sui_signer; +use gem_ton::signer::{TonSignDataResponse, TonSignMessageData, sign_personal as ton_sign_personal}; +use signer::{SignatureScheme, Signer, hash_eip712}; use sui_types::PersonalMessage; use super::{ @@ -12,7 +14,6 @@ use super::{ sign_type::{SignDigestType, SignMessage}, }; use crate::{GemstoneError, siwe::SiweMessage}; -use ::signer::{SignatureScheme, Signer, hash_eip712}; use zeroize::Zeroizing; const SIGNATURE_LENGTH: usize = 65; @@ -158,9 +159,9 @@ impl MessageSigner { let private_key = Zeroizing::new(private_key); let hash = self.hash()?; match &self.message.sign_type { - SignDigestType::SuiPersonal => Signer::sign_sui_digest(&hash, &private_key).map_err(GemstoneError::from), + SignDigestType::SuiPersonal => sui_signer::sign_digest(&hash, &private_key).map_err(GemstoneError::from), SignDigestType::TonPersonal => { - let (signature, public_key) = Signer::sign_ton_personal(&self.message.data, &private_key)?; + let (signature, public_key) = ton_sign_personal(&self.message.data, &private_key)?; self.get_ton_result(&signature, &public_key) } SignDigestType::Eip191 | SignDigestType::Eip712 | SignDigestType::Siwe => { @@ -490,7 +491,10 @@ mod tests { #[test] fn test_ton_personal_preview() { - let ton_data = TonSignMessageData::new(serde_json::json!({"type": "text", "text": "Hello TON", "from": "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}), "example.com".to_string()); + let ton_data = TonSignMessageData::new( + serde_json::json!({"type": "text", "text": "Hello TON", "from": "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}), + "example.com".to_string(), + ); let data = String::from_utf8(ton_data.to_bytes()).unwrap(); let decoder = MessageSigner::new(SignMessage { chain: Chain::Ton, diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index 8570d9578..bcabb2365 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -89,7 +89,7 @@ mod tests { #[test] fn validate_ton_sign_message() { - use signer::TonSignMessageData; + use gem_ton::signer::TonSignMessageData; let wallet_connect = WalletConnect::new(); @@ -176,7 +176,14 @@ impl WalletConnect { Some(primitives::WalletConnectCAIP2::get_chain(caip2, caip10)?.to_string()) } - pub fn parse_request(&self, topic: String, method: String, params: String, chain_id: String, domain: String) -> Result { + pub fn parse_request( + &self, + topic: String, + method: String, + params: String, + chain_id: String, + domain: String, + ) -> Result { let request = WalletConnectRequest { topic, method, @@ -217,7 +224,7 @@ impl WalletConnect { gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } SignDigestType::TonPersonal => { - let ton_data = signer::TonSignMessageData::from_bytes(data.as_bytes())?; + let ton_data = gem_ton::signer::TonSignMessageData::from_bytes(data.as_bytes())?; ton_data.get_payload()?; Ok(()) } diff --git a/gemstone/src/wallet_connect/request_handler/ton.rs b/gemstone/src/wallet_connect/request_handler/ton.rs index ca8f4e506..fe352e777 100644 --- a/gemstone/src/wallet_connect/request_handler/ton.rs +++ b/gemstone/src/wallet_connect/request_handler/ton.rs @@ -1,14 +1,16 @@ use crate::message::sign_type::SignDigestType; use crate::wallet_connect::actions::{WalletConnectAction, WalletConnectTransactionType}; use crate::wallet_connect::handler_traits::ChainRequestHandler; +use gem_ton::signer::TonSignMessageData; use primitives::{Chain, TransferDataOutputType}; use serde_json::Value; -use signer::TonSignMessageData; pub struct TonRequestHandler; fn extract_host(url: &str) -> String { - url::Url::parse(url).map(|u| u.host_str().unwrap_or(url).to_string()).unwrap_or_else(|_| url.to_string()) + url::Url::parse(url) + .map(|u| u.host_str().unwrap_or(url).to_string()) + .unwrap_or_else(|_| url.to_string()) } impl ChainRequestHandler for TonRequestHandler { diff --git a/gemstone/tests/ios/GemTest/GemTest.xcodeproj/project.pbxproj b/gemstone/tests/ios/GemTest/GemTest.xcodeproj/project.pbxproj index f04ddd30b..8b9e12aec 100644 --- a/gemstone/tests/ios/GemTest/GemTest.xcodeproj/project.pbxproj +++ b/gemstone/tests/ios/GemTest/GemTest.xcodeproj/project.pbxproj @@ -410,7 +410,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"GemTest/Preview Content\""; - DEVELOPMENT_TEAM = 5PL8474KLR; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -434,7 +434,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"GemTest/Preview Content\""; - DEVELOPMENT_TEAM = 5PL8474KLR; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; diff --git a/gemstone/tests/ios/GemTest/GemTestTests/GemTestTests.swift b/gemstone/tests/ios/GemTest/GemTestTests/GemTestTests.swift index 816616421..adbf02a89 100644 --- a/gemstone/tests/ios/GemTest/GemTestTests/GemTestTests.swift +++ b/gemstone/tests/ios/GemTest/GemTestTests/GemTestTests.swift @@ -83,7 +83,7 @@ final class GemTestTests: XCTestCase { data: "hello world".data(using: .utf8)! ) let signer = MessageSigner(message: message) - let hash = signer.hash() + let hash = try signer.hash() XCTAssertEqual( hash.hexString(), From 362109a899341899cbcc81664c1eb0cb8382696c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:55:16 +0900 Subject: [PATCH 09/11] keep ed25519-dalek and zeroize insdie signer --- Cargo.lock | 6 ++-- crates/gem_sui/Cargo.toml | 5 ++-- crates/gem_sui/src/signer/signature.rs | 40 ++++---------------------- crates/gem_ton/Cargo.toml | 5 ++-- crates/gem_ton/src/signer/signature.rs | 33 +++++++-------------- crates/signer/src/lib.rs | 30 ++++++++++++++++--- 6 files changed, 48 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d25a595b6..d2639aa61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3040,7 +3040,6 @@ dependencies = [ "chain_primitives", "chain_traits", "chrono", - "ed25519-dalek", "futures", "gem_client", "gem_jsonrpc", @@ -3053,10 +3052,10 @@ dependencies = [ "serde_json", "serde_serializers", "settings", + "signer", "sui-sdk-types", "sui-transaction-builder", "tokio", - "zeroize", ] [[package]] @@ -3068,7 +3067,6 @@ dependencies = [ "chain_traits", "chrono", "crc", - "ed25519-dalek", "futures", "gem_client", "hex", @@ -3079,8 +3077,8 @@ dependencies = [ "serde_json", "serde_serializers", "settings", + "signer", "tokio", - "zeroize", ] [[package]] diff --git a/crates/gem_sui/Cargo.toml b/crates/gem_sui/Cargo.toml index e6ad99e03..be08bd37f 100644 --- a/crates/gem_sui/Cargo.toml +++ b/crates/gem_sui/Cargo.toml @@ -11,7 +11,7 @@ rpc = [ "dep:gem_client", "dep:chain_traits", ] -signer = ["dep:ed25519-dalek", "dep:zeroize"] +signer = ["dep:signer"] reqwest = ["gem_jsonrpc/reqwest"] chain_integration_tests = ["rpc", "reqwest", "settings/testkit"] @@ -31,8 +31,7 @@ num-traits = { workspace = true } gem_jsonrpc = { path = "../gem_jsonrpc" } futures = { workspace = true } -ed25519-dalek = { version = "2.1.1", default-features = false, features = ["std", "zeroize"], optional = true } -zeroize = { version = "1.8.2", optional = true } +signer = { path = "../signer", optional = true } # Optional dependencies for rpc feature num-bigint = { workspace = true, features = ["serde"] } diff --git a/crates/gem_sui/src/signer/signature.rs b/crates/gem_sui/src/signer/signature.rs index d56f53323..b88f9771b 100644 --- a/crates/gem_sui/src/signer/signature.rs +++ b/crates/gem_sui/src/signer/signature.rs @@ -1,9 +1,8 @@ use std::borrow::Cow; -use ed25519_dalek::Signer as DalekSigner; use primitives::SignerError; +use signer::Signer; use sui_types::{Ed25519PublicKey, Ed25519Signature, PersonalMessage, SimpleSignature, UserSignature}; -use zeroize::Zeroizing; /// 1-byte flag + 64-byte signature + 32-byte public key. pub const SUI_PERSONAL_MESSAGE_SIGNATURE_LEN: usize = 1 + Ed25519Signature::LENGTH + Ed25519PublicKey::LENGTH; @@ -15,22 +14,12 @@ pub fn sign_personal_message(message: &[u8], private_key: &[u8]) -> Result Result { - let private_key = Zeroizing::new(private_key.to_vec()); - let signing_key = signing_key_from_bytes(&private_key)?; - let signature = signing_key.sign(digest); - let signature_bytes = signature.to_bytes(); - let public_key_bytes = signing_key.verifying_key().to_bytes(); + let (signature_bytes, public_key_bytes) = + Signer::sign_ed25519_with_public_key(digest, private_key).map_err(|e| SignerError::InvalidInput(e.to_string()))?; assemble_signature(&signature_bytes, &public_key_bytes) } -fn signing_key_from_bytes(private_key: &[u8]) -> Result { - let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key - .try_into() - .map_err(|_| SignerError::InvalidInput("Invalid Ed25519 private key length".to_string()))?; - Ok(ed25519_dalek::SigningKey::from_bytes(&key_bytes)) -} - fn assemble_signature(signature: &[u8], public_key: &[u8]) -> Result { let signature_bytes: [u8; Ed25519Signature::LENGTH] = signature .try_into() @@ -54,9 +43,8 @@ fn assemble_signature(signature: &[u8], public_key: &[u8]) -> Result Result<(Vec, Vec Result { - let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key - .try_into() - .map_err(|_| SignerError::InvalidInput("Invalid Ed25519 private key length".to_string()))?; - Ok(ed25519_dalek::SigningKey::from_bytes(&key_bytes)) + Signer::sign_ed25519_with_public_key(&digest, private_key).map_err(|e| SignerError::InvalidInput(e.to_string())) } #[cfg(test)] mod tests { use super::*; - use ed25519_dalek::{Signature, SigningKey, Verifier}; #[test] fn test_sign_ton_personal() { @@ -42,13 +27,15 @@ mod tests { assert_eq!(signature.len(), 64, "Ed25519 signature should be 64 bytes"); assert_eq!(public_key.len(), 32, "Ed25519 public key should be 32 bytes"); + } - let key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = private_key.try_into().expect("32 byte secret key"); - let signing_key = SigningKey::from_bytes(&key_bytes); - assert_eq!(public_key, signing_key.verifying_key().as_bytes(), "public key should match"); + #[test] + fn test_sign_ton_personal_rejects_invalid_key() { + let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); + let data = ton_data.to_bytes(); - let signature = Signature::from_bytes(signature.as_slice().try_into().expect("64 byte signature")); - let digest = b"Hello TON"; - signing_key.verifying_key().verify(digest, &signature).expect("signature verifies"); + let result = sign_personal(&data, &[0u8; 16]); + assert!(result.is_err()); } } diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 412e69da0..e3b53f4eb 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -3,12 +3,12 @@ mod eip712; mod error; mod secp256k1; +use ed25519_dalek::Signer as DalekSigner; use zeroize::Zeroizing; -use crate::ed25519::sign_digest as sign_ed25519_digest; +use crate::ed25519::{sign_digest as sign_ed25519_digest, signing_key_from_bytes}; use crate::secp256k1::sign_digest as sign_secp256k1_digest; -pub use ed25519::signing_key_from_bytes; pub use eip712::hash_typed_data as hash_eip712; pub use error::SignerError; @@ -30,6 +30,17 @@ impl Signer { } } + /// Sign a digest with Ed25519 and return both the signature and public key bytes. + /// Returns (signature_bytes, public_key_bytes) where signature is 64 bytes and public key is 32 bytes. + pub fn sign_ed25519_with_public_key(digest: &[u8], private_key: &[u8]) -> Result<(Vec, Vec), SignerError> { + let private_key = Zeroizing::new(private_key.to_vec()); + let signing_key = signing_key_from_bytes(&private_key)?; + let signature = signing_key.sign(digest); + let signature_bytes = signature.to_bytes().to_vec(); + let public_key_bytes = signing_key.verifying_key().to_bytes().to_vec(); + Ok((signature_bytes, public_key_bytes)) + } + pub fn sign_eip712(typed_data_json: &str, private_key: &[u8]) -> Result { let digest = eip712::hash_typed_data(typed_data_json)?; let private_key_vec = Zeroizing::new(private_key.to_vec()); @@ -43,8 +54,19 @@ mod tests { use super::*; #[test] - fn ed25519_signing_key_rejects_invalid_length() { - let result = signing_key_from_bytes(&[0u8; 16]); + fn ed25519_sign_with_public_key_rejects_invalid_length() { + let result = Signer::sign_ed25519_with_public_key(&[0u8; 32], &[0u8; 16]); assert!(result.is_err()); } + + #[test] + fn ed25519_sign_with_public_key_returns_correct_lengths() { + let private_key = hex::decode("1e9d38b5274152a78dff1a86fa464ceadc1f4238ca2c17060c3c507349424a34").unwrap(); + let digest = b"test message"; + + let (signature, public_key) = Signer::sign_ed25519_with_public_key(digest, &private_key).unwrap(); + + assert_eq!(signature.len(), 64, "Ed25519 signature should be 64 bytes"); + assert_eq!(public_key.len(), 32, "Ed25519 public key should be 32 bytes"); + } } From e0ededd60958c81ddbb830ec8163b290df32889d Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:54:39 +0900 Subject: [PATCH 10/11] refactor TonSignMessageData to store TonSignDataPayload --- crates/gem_sui/src/signer/signature.rs | 2 +- crates/gem_ton/src/signer/signature.rs | 7 ++-- crates/gem_ton/src/signer/types.rs | 36 +++++++++---------- gemstone/src/message/signer.rs | 15 ++++---- gemstone/src/wallet_connect/mod.rs | 27 +++++--------- .../src/wallet_connect/request_handler/ton.rs | 6 ++-- 6 files changed, 38 insertions(+), 55 deletions(-) diff --git a/crates/gem_sui/src/signer/signature.rs b/crates/gem_sui/src/signer/signature.rs index b88f9771b..dce1655dc 100644 --- a/crates/gem_sui/src/signer/signature.rs +++ b/crates/gem_sui/src/signer/signature.rs @@ -43,8 +43,8 @@ fn assemble_signature(signature: &[u8], public_key: &[u8]) -> Result Result<(Vec, Vec), SignerError> { let ton_data = TonSignMessageData::from_bytes(data)?; - let payload = ton_data.get_payload()?; - let digest = payload.hash(); + let digest = ton_data.payload.hash(); Signer::sign_ed25519_with_public_key(&digest, private_key).map_err(|e| SignerError::InvalidInput(e.to_string())) } @@ -17,7 +16,7 @@ mod tests { #[test] fn test_sign_ton_personal() { - let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); let data = ton_data.to_bytes(); @@ -31,7 +30,7 @@ mod tests { #[test] fn test_sign_ton_personal_rejects_invalid_key() { - let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; let ton_data = TonSignMessageData::new(payload, "example.com".to_string()); let data = ton_data.to_bytes(); diff --git a/crates/gem_ton/src/signer/types.rs b/crates/gem_ton/src/signer/types.rs index 81719165e..4d23fa622 100644 --- a/crates/gem_ton/src/signer/types.rs +++ b/crates/gem_ton/src/signer/types.rs @@ -10,10 +10,6 @@ pub enum TonSignDataPayload { } impl TonSignDataPayload { - pub fn parse(json: &str) -> Result { - serde_json::from_str(json).map_err(SignerError::from) - } - pub fn data(&self) -> &str { match self { Self::Text { text } => text, @@ -39,15 +35,20 @@ pub struct TonSignDataResponse { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TonSignMessageData { - pub payload: serde_json::Value, + pub payload: TonSignDataPayload, pub domain: String, } impl TonSignMessageData { - pub fn new(payload: serde_json::Value, domain: String) -> Self { + pub fn new(payload: TonSignDataPayload, domain: String) -> Self { Self { payload, domain } } + pub fn from_value(payload: serde_json::Value, domain: String) -> Result { + let payload: TonSignDataPayload = serde_json::from_value(payload).map_err(SignerError::from)?; + Ok(Self { payload, domain }) + } + pub fn from_bytes(data: &[u8]) -> Result { serde_json::from_slice(data).map_err(SignerError::from) } @@ -55,11 +56,6 @@ impl TonSignMessageData { pub fn to_bytes(&self) -> Vec { serde_json::to_vec(self).unwrap_or_default() } - - pub fn get_payload(&self) -> Result { - let json = serde_json::to_string(&self.payload)?; - TonSignDataPayload::parse(&json) - } } impl TonSignDataResponse { @@ -85,27 +81,28 @@ mod tests { #[test] fn test_parse_payload_text() { let json = r#"{"type":"text","text":"Hello TON"}"#; - let parsed = TonSignDataPayload::parse(json).unwrap(); + let parsed: TonSignDataPayload = serde_json::from_str(json).unwrap(); assert_eq!(parsed, TonSignDataPayload::Text { text: "Hello TON".to_string() }); - assert_eq!(parsed.data(), "Hello TON"); - assert_eq!(parsed.hash(), b"Hello TON".to_vec()); + assert_eq!(b"Hello TON".to_vec(), parsed.hash()); } #[test] fn test_parse_payload_binary() { let json = r#"{"type":"binary","bytes":"SGVsbG8="}"#; - let parsed = TonSignDataPayload::parse(json).unwrap(); + let parsed: TonSignDataPayload = serde_json::from_str(json).unwrap(); assert_eq!(parsed, TonSignDataPayload::Binary { bytes: "SGVsbG8=".to_string() }); + assert_eq!("SGVsbG8=".as_bytes().to_vec(), parsed.hash()); } #[test] fn test_parse_payload_cell() { let json = r#"{"type":"cell","cell":"te6c"}"#; - let parsed = TonSignDataPayload::parse(json).unwrap(); + let parsed: TonSignDataPayload = serde_json::from_str(json).unwrap(); assert_eq!(parsed, TonSignDataPayload::Cell { cell: "te6c".to_string() }); + assert_eq!("te6c".as_bytes().to_vec(), parsed.hash()); } #[test] @@ -133,7 +130,7 @@ mod tests { #[test] fn test_ton_sign_message_data() { - let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; let data = TonSignMessageData::new(payload.clone(), "example.com".to_string()); let bytes = data.to_bytes(); @@ -145,10 +142,9 @@ mod tests { #[test] fn test_ton_sign_message_data_get_payload() { - let payload = serde_json::json!({"type": "text", "text": "Hello TON"}); + let payload = TonSignDataPayload::Text { text: "Hello TON".to_string() }; let data = TonSignMessageData::new(payload, "example.com".to_string()); - let parsed_payload = data.get_payload().unwrap(); - assert_eq!(parsed_payload, TonSignDataPayload::Text { text: "Hello TON".to_string() }); + assert_eq!(data.payload, TonSignDataPayload::Text { text: "Hello TON".to_string() }); } } diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index a11cf48a5..83a1546c6 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -51,10 +51,7 @@ impl MessageSigner { let Ok(ton_data) = TonSignMessageData::from_bytes(string.as_bytes()) else { return Ok(MessagePreview::Text(string)); }; - let Ok(payload) = ton_data.get_payload() else { - return Ok(MessagePreview::Text(string)); - }; - Ok(MessagePreview::Text(payload.data().to_string())) + Ok(MessagePreview::Text(ton_data.payload.data().to_string())) } SignDigestType::Eip712 => { let string = String::from_utf8(self.message.data.clone())?; @@ -108,8 +105,7 @@ impl MessageSigner { SignDigestType::TonPersonal => { let string = String::from_utf8(self.message.data.clone())?; let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; - let payload = ton_data.get_payload()?; - Ok(payload.hash()) + Ok(ton_data.payload.hash()) } SignDigestType::Eip191 | SignDigestType::Siwe => Ok(eip191_hash_message(&self.message.data).to_vec()), SignDigestType::Eip712 => { @@ -144,7 +140,7 @@ impl MessageSigner { pub fn get_ton_result(&self, signature: &[u8], public_key: &[u8]) -> Result { let string = String::from_utf8(self.message.data.clone())?; let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; - let payload = ton_data.get_payload()?; + let payload = ton_data.payload; let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs()) @@ -491,10 +487,11 @@ mod tests { #[test] fn test_ton_personal_preview() { - let ton_data = TonSignMessageData::new( + let ton_data = TonSignMessageData::from_value( serde_json::json!({"type": "text", "text": "Hello TON", "from": "UQBY1cVPu4SIr36q0M3HWcqPb_efyVVRBsEzmwN-wKQDR6zg"}), "example.com".to_string(), - ); + ) + .unwrap(); let data = String::from_utf8(ton_data.to_bytes()).unwrap(); let decoder = MessageSigner::new(SignMessage { chain: Chain::Ton, diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index bcabb2365..b8fa26b8f 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -89,28 +89,20 @@ mod tests { #[test] fn validate_ton_sign_message() { - use gem_ton::signer::TonSignMessageData; + use gem_ton::signer::{TonSignDataPayload, TonSignMessageData}; let wallet_connect = WalletConnect::new(); // Missing type field in payload - let ton_data = TonSignMessageData::new(serde_json::json!({"text":"Hello"}), "example.com".to_string()); - assert!( - wallet_connect - .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) - .is_err() - ); + let ton_data = r#"{"payload":{"text":"Hello"},"domain":"example.com"}"#.to_string(); + assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, ton_data).is_err()); // Unknown type - let ton_data = TonSignMessageData::new(serde_json::json!({"type":"unknown"}), "example.com".to_string()); - assert!( - wallet_connect - .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) - .is_err() - ); + let ton_data = r#"{"payload":{"type":"unknown"},"domain":"example.com"}"#.to_string(); + assert!(wallet_connect.validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, ton_data).is_err()); // Valid text type - let ton_data = TonSignMessageData::new(serde_json::json!({"type":"text","text":"Hello"}), "example.com".to_string()); + let ton_data = TonSignMessageData::new(TonSignDataPayload::Text { text: "Hello".to_string() }, "example.com".to_string()); assert!( wallet_connect .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) @@ -118,7 +110,7 @@ mod tests { ); // Valid binary type - let ton_data = TonSignMessageData::new(serde_json::json!({"type":"binary","bytes":"SGVsbG8="}), "example.com".to_string()); + let ton_data = TonSignMessageData::new(TonSignDataPayload::Binary { bytes: "SGVsbG8=".to_string() }, "example.com".to_string()); assert!( wallet_connect .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) @@ -126,7 +118,7 @@ mod tests { ); // Valid cell type - let ton_data = TonSignMessageData::new(serde_json::json!({"type":"cell","cell":"te6c"}), "example.com".to_string()); + let ton_data = TonSignMessageData::new(TonSignDataPayload::Cell { cell: "te6c".to_string() }, "example.com".to_string()); assert!( wallet_connect .validate_sign_message(Chain::Ton, SignDigestType::TonPersonal, String::from_utf8(ton_data.to_bytes()).unwrap()) @@ -224,8 +216,7 @@ impl WalletConnect { gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) } SignDigestType::TonPersonal => { - let ton_data = gem_ton::signer::TonSignMessageData::from_bytes(data.as_bytes())?; - ton_data.get_payload()?; + gem_ton::signer::TonSignMessageData::from_bytes(data.as_bytes())?; Ok(()) } SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe => Ok(()), diff --git a/gemstone/src/wallet_connect/request_handler/ton.rs b/gemstone/src/wallet_connect/request_handler/ton.rs index fe352e777..cdc8cd58a 100644 --- a/gemstone/src/wallet_connect/request_handler/ton.rs +++ b/gemstone/src/wallet_connect/request_handler/ton.rs @@ -18,7 +18,7 @@ impl ChainRequestHandler for TonRequestHandler { let params_array = params.as_array().ok_or("Invalid params format")?; let payload = params_array.first().ok_or("Missing payload parameter")?.clone(); let host = extract_host(domain); - let ton_data = TonSignMessageData::new(payload, host); + let ton_data = TonSignMessageData::from_value(payload, host).map_err(|e| e.to_string())?; let data = String::from_utf8(ton_data.to_bytes()).map_err(|e| format!("Failed to encode TonSignMessageData: {}", e))?; Ok(WalletConnectAction::SignMessage { chain: Chain::Ton, @@ -53,6 +53,7 @@ impl ChainRequestHandler for TonRequestHandler { #[cfg(test)] mod tests { use super::*; + use gem_ton::signer::TonSignDataPayload; #[test] fn test_parse_sign_message() { @@ -66,8 +67,7 @@ mod tests { let parsed: TonSignMessageData = serde_json::from_str(&data).unwrap(); assert_eq!(parsed.domain, "react-app.walletconnect.com"); - assert_eq!(parsed.payload["type"], "text"); - assert_eq!(parsed.payload["text"], "Hello TON"); + assert_eq!(parsed.payload, TonSignDataPayload::Text { text: "Hello TON".to_string() }); } #[test] From a34db06f55d5e048c049025f3217771d3367896e Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Wed, 17 Dec 2025 12:57:31 +0900 Subject: [PATCH 11/11] Update signature.rs --- crates/gem_ton/src/signer/signature.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/gem_ton/src/signer/signature.rs b/crates/gem_ton/src/signer/signature.rs index 49e6ebb31..55492f883 100644 --- a/crates/gem_ton/src/signer/signature.rs +++ b/crates/gem_ton/src/signer/signature.rs @@ -13,6 +13,7 @@ pub fn sign_personal(data: &[u8], private_key: &[u8]) -> Result<(Vec, Vec