From a65e241af6b83a1886ee570d59fde14afdc1a806 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:05:13 +0900 Subject: [PATCH 1/8] impl wallet connect for tron --- Cargo.lock | 3 + crates/gem_tron/Cargo.toml | 3 + crates/gem_tron/src/lib.rs | 3 + crates/gem_tron/src/provider/preload.rs | 1 + crates/gem_tron/src/signer/chain_signer.rs | 144 ++++++++++++++++++ crates/gem_tron/src/signer/message.rs | 11 ++ crates/gem_tron/src/signer/mod.rs | 5 + crates/primitives/src/chain_config.rs | 2 +- .../src/transaction_load_metadata.rs | 1 + .../src/wallet_connect_namespace.rs | 13 +- crates/primitives/src/wallet_connector.rs | 6 + gemstone/src/config/wallet_connect.rs | 2 +- gemstone/src/message/sign_type.rs | 1 + gemstone/src/message/signer.rs | 16 +- gemstone/src/models/transaction.rs | 5 + gemstone/src/signer/chain.rs | 2 + gemstone/src/siwe.rs | 3 +- gemstone/src/wallet_connect/actions.rs | 5 + gemstone/src/wallet_connect/mod.rs | 46 ++++-- .../src/wallet_connect/request_handler/mod.rs | 5 + .../wallet_connect/request_handler/tron.rs | 100 ++++++++++++ .../src/wallet_connect/response_handler.rs | 25 ++- 22 files changed, 377 insertions(+), 25 deletions(-) create mode 100644 crates/gem_tron/src/signer/chain_signer.rs create mode 100644 crates/gem_tron/src/signer/message.rs create mode 100644 crates/gem_tron/src/signer/mod.rs create mode 100644 gemstone/src/wallet_connect/request_handler/tron.rs diff --git a/Cargo.lock b/Cargo.lock index 946a3a17b..ce954db57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3554,6 +3554,7 @@ dependencies = [ "futures", "gem_client", "gem_evm", + "gem_hash", "hex", "num-bigint", "num-traits", @@ -3562,6 +3563,8 @@ dependencies = [ "serde", "serde_json", "settings", + "sha2", + "signer", "tokio", ] diff --git a/crates/gem_tron/Cargo.toml b/crates/gem_tron/Cargo.toml index fe4fb8f0b..28485f352 100644 --- a/crates/gem_tron/Cargo.toml +++ b/crates/gem_tron/Cargo.toml @@ -7,6 +7,8 @@ edition = { workspace = true } bs58 = { workspace = true } hex = { workspace = true } primitives = { path = "../primitives" } +sha2 = { workspace = true } +signer = { path = "../signer" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -17,6 +19,7 @@ num-traits = { workspace = true, optional = true } gem_evm = { path = "../gem_evm", optional = true } gem_client = { path = "../gem_client", optional = true } chain_traits = { path = "../chain_traits", optional = true } +gem_hash = { path = "../gem_hash" } async-trait = { workspace = true, optional = true } futures = { workspace = true, optional = true } alloy-primitives = { workspace = true, optional = true } diff --git a/crates/gem_tron/src/lib.rs b/crates/gem_tron/src/lib.rs index 04604aca9..b4c0f51f9 100644 --- a/crates/gem_tron/src/lib.rs +++ b/crates/gem_tron/src/lib.rs @@ -1,4 +1,7 @@ pub mod address; +pub mod signer; + +pub use signer::TronChainSigner; #[cfg(feature = "rpc")] pub mod rpc; diff --git a/crates/gem_tron/src/provider/preload.rs b/crates/gem_tron/src/provider/preload.rs index 4afaaec09..6c792f554 100644 --- a/crates/gem_tron/src/provider/preload.rs +++ b/crates/gem_tron/src/provider/preload.rs @@ -43,6 +43,7 @@ impl ChainTransactionLoad for TronClient { parent_hash: block.parent_hash.clone(), witness_address: block.witness_address.clone(), votes, + raw_data_hex: None, }; let fee = match &input.input_type { diff --git a/crates/gem_tron/src/signer/chain_signer.rs b/crates/gem_tron/src/signer/chain_signer.rs new file mode 100644 index 000000000..21f19a7f2 --- /dev/null +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -0,0 +1,144 @@ +use primitives::{ + ChainSigner, + SignerError, + TransactionInputType, + TransactionLoadInput, + TransactionLoadMetadata, + TransferDataOutputAction, + TransferDataOutputType, + hex::decode_hex, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use sha2::{Digest, Sha256}; +use signer::{SignatureScheme, Signer}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct TronTransaction { + raw_data_hex: Option, + #[serde(default)] + signature: Vec, + #[serde(flatten)] + other: Map, +} + +struct PayloadMetadata { + payload: Value, + output_type: TransferDataOutputType, + output_action: TransferDataOutputAction, + raw_data_hex: String, +} + +pub struct TronChainSigner; + +impl ChainSigner for TronChainSigner { + fn sign_data(&self, input: &TransactionLoadInput, private_key: &[u8]) -> Result { + sign_data(input, private_key) + } +} + +fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result { + let (transaction, metadata) = extract_transaction(input)?; + if !transaction.signature.is_empty() { + return Err(SignerError::InvalidInput("Tron multisig not supported for WalletConnect signing".to_string())); + } + let raw_bytes = decode_hex(&metadata.raw_data_hex)?; + let digest = Sha256::digest(&raw_bytes); + let signature = Signer::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key.to_vec()) + .map_err(|err| SignerError::InvalidInput(err.to_string()))?; + let signature_hex = hex::encode(signature); + + match metadata.output_type { + TransferDataOutputType::Signature => Ok(signature_hex), + TransferDataOutputType::EncodedTransaction => { + let payload = apply_signature(metadata.payload, &signature_hex)?; + let result_payload = match metadata.output_action { + TransferDataOutputAction::Send => { + extract_transaction_payload(&payload) + .ok_or_else(|| SignerError::InvalidInput("Missing transaction object for Tron broadcast".to_string()))? + } + TransferDataOutputAction::Sign => payload, + }; + + Ok(serde_json::to_string(&result_payload)?) + } + } +} + +fn extract_transaction(input: &TransactionLoadInput) -> Result<(TronTransaction, PayloadMetadata), SignerError> { + let TransactionInputType::Generic(_, _, extra) = &input.input_type else { + return Err(SignerError::InvalidInput("Expected generic transaction input".to_string())); + }; + let data = extra.data.as_ref().ok_or_else(|| SignerError::InvalidInput("Missing transaction data".to_string()))?; + + let mut payload: Value = serde_json::from_slice(data)?; + if let Value::String(raw_json) = &payload + && let Ok(parsed) = serde_json::from_str::(raw_json) + { + payload = parsed; + } + + let transaction_value = extract_transaction_payload(&payload) + .ok_or_else(|| SignerError::InvalidInput("Missing transaction object for Tron broadcast".to_string()))?; + let transaction: TronTransaction = serde_json::from_value(transaction_value)?; + let raw_data_hex = match &input.metadata { + TransactionLoadMetadata::Tron { + raw_data_hex: Some(raw_data_hex), + .. + } => raw_data_hex.clone(), + _ => transaction + .raw_data_hex + .clone() + .ok_or_else(|| SignerError::InvalidInput("Missing raw_data_hex in Tron transaction payload".to_string()))?, + }; + + let metadata = PayloadMetadata { + payload, + output_type: extra.output_type.clone(), + output_action: extra.output_action.clone(), + raw_data_hex, + }; + Ok((transaction, metadata)) +} + +fn extract_transaction_payload(value: &Value) -> Option { + match value { + Value::Object(map) => { + if map.get("raw_data_hex").is_some() { + return Some(value.clone()); + } + if let Some(transaction) = map.get("transaction") + && let Some(found) = extract_transaction_payload(transaction) + { + return Some(found); + } + map.values().find_map(extract_transaction_payload) + } + Value::Array(values) => values.iter().find_map(extract_transaction_payload), + _ => None, + } +} + +fn apply_signature(payload: Value, signature_hex: &str) -> Result { + let transaction_value = extract_transaction_payload(&payload) + .ok_or_else(|| SignerError::InvalidInput("Missing transaction object for Tron broadcast".to_string()))?; + let mut transaction: TronTransaction = serde_json::from_value(transaction_value)?; + if !transaction.signature.is_empty() { + return Err(SignerError::InvalidInput("Tron multisig not supported for WalletConnect signing".to_string())); + } + transaction.signature = vec![signature_hex.to_string()]; + let updated_transaction = serde_json::to_value(transaction)?; + match payload { + Value::Object(mut map) => { + if map.get("raw_data_hex").is_some() { + Ok(updated_transaction) + } else if map.get("transaction").is_some() { + map.insert("transaction".to_string(), updated_transaction); + Ok(Value::Object(map)) + } else { + Err(SignerError::InvalidInput("Missing raw_data_hex in Tron transaction payload".to_string())) + } + } + _ => Err(SignerError::InvalidInput("Invalid Tron transaction payload".to_string())), + } +} diff --git a/crates/gem_tron/src/signer/message.rs b/crates/gem_tron/src/signer/message.rs new file mode 100644 index 000000000..2cae627d7 --- /dev/null +++ b/crates/gem_tron/src/signer/message.rs @@ -0,0 +1,11 @@ +use gem_hash::keccak::keccak256; + +const TRON_MESSAGE_PREFIX: &str = "\x19TRON Signed Message:\n"; + +pub fn tron_hash_message(message: &[u8]) -> [u8; 32] { + let prefix = format!("{TRON_MESSAGE_PREFIX}{}", message.len()); + let mut data = Vec::with_capacity(prefix.len() + message.len()); + data.extend_from_slice(prefix.as_bytes()); + data.extend_from_slice(message); + keccak256(&data) +} diff --git a/crates/gem_tron/src/signer/mod.rs b/crates/gem_tron/src/signer/mod.rs new file mode 100644 index 000000000..46980fafa --- /dev/null +++ b/crates/gem_tron/src/signer/mod.rs @@ -0,0 +1,5 @@ +mod chain_signer; +mod message; + +pub use chain_signer::TronChainSigner; +pub use message::tron_hash_message; diff --git a/crates/primitives/src/chain_config.rs b/crates/primitives/src/chain_config.rs index ecd7a0011..eefe3c52c 100644 --- a/crates/primitives/src/chain_config.rs +++ b/crates/primitives/src/chain_config.rs @@ -330,7 +330,7 @@ static CHAIN_CONFIGS: &[ChainConfig] = &[ }, ChainConfig { chain: Chain::Tron, - network_id: "", + network_id: "0x2b6653dc", denom: None, slip44: 195, chain_type: ChainType::Tron, diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index eb3e22797..2f7904d27 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -86,6 +86,7 @@ pub enum TransactionLoadMetadata { parent_hash: String, witness_address: String, votes: HashMap, + raw_data_hex: Option, }, Sui { message_bytes: String, diff --git a/crates/primitives/src/wallet_connect_namespace.rs b/crates/primitives/src/wallet_connect_namespace.rs index ec11a0e5f..b2cb842b8 100644 --- a/crates/primitives/src/wallet_connect_namespace.rs +++ b/crates/primitives/src/wallet_connect_namespace.rs @@ -19,6 +19,8 @@ pub enum WalletConnectCAIP2 { Sui, #[serde(rename = "ton")] Ton, + #[serde(rename = "tron")] + Tron, #[serde(rename = "bip122")] Bip122, } @@ -32,8 +34,9 @@ impl WalletConnectCAIP2 { 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::Tron => Some(WalletConnectCAIP2::Tron.as_ref().to_string()), ChainType::Bitcoin => Some(WalletConnectCAIP2::Bip122.as_ref().to_string()), - ChainType::Tron | ChainType::Aptos | ChainType::Xrp | ChainType::Near | ChainType::Stellar | ChainType::Polkadot | ChainType::Cardano | ChainType::HyperCore => None, + ChainType::Aptos | ChainType::Xrp | ChainType::Near | ChainType::Stellar | ChainType::Polkadot | ChainType::Cardano | ChainType::HyperCore => None, } } @@ -45,6 +48,7 @@ impl WalletConnectCAIP2 { WalletConnectCAIP2::Algorand => Some(ChainType::Algorand), WalletConnectCAIP2::Sui => Some(ChainType::Sui), WalletConnectCAIP2::Ton => Some(ChainType::Ton), + WalletConnectCAIP2::Tron => Some(ChainType::Tron), WalletConnectCAIP2::Bip122 => Some(ChainType::Bitcoin), } } @@ -65,6 +69,7 @@ impl WalletConnectCAIP2 { WalletConnectCAIP2::Algorand => Some(Chain::Algorand), WalletConnectCAIP2::Sui => Some(Chain::Sui), WalletConnectCAIP2::Ton => Some(Chain::Ton), + WalletConnectCAIP2::Tron => Some(Chain::Tron), WalletConnectCAIP2::Bip122 => Some(Chain::Bitcoin), } } @@ -78,7 +83,8 @@ impl WalletConnectCAIP2 { ChainType::Sui => Some("mainnet".to_string()), ChainType::Ton => Some("-239".to_string()), ChainType::Bitcoin => Some("000000000019d6689c085ae165831e93".to_string()), - ChainType::Tron | ChainType::Aptos | ChainType::Xrp | ChainType::Near | ChainType::Stellar | ChainType::Polkadot | ChainType::Cardano | ChainType::HyperCore => None, + ChainType::Tron => Some(chain.network_id().to_string()), + ChainType::Aptos | ChainType::Xrp | ChainType::Near | ChainType::Stellar | ChainType::Polkadot | ChainType::Cardano | ChainType::HyperCore => None, } } @@ -109,6 +115,7 @@ mod tests { 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("tron".to_string()), Some(ChainType::Tron)); assert_eq!(WalletConnectCAIP2::get_chain_type("bip122".to_string()), Some(ChainType::Bitcoin)); assert_eq!(WalletConnectCAIP2::get_chain_type("unknown".to_string()), None); } @@ -120,6 +127,7 @@ mod tests { 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)); + assert_eq!(WalletConnectCAIP2::get_chain("tron".to_string(), "0x2b6653dc".to_string()), Some(Chain::Tron)); assert_eq!( WalletConnectCAIP2::get_chain("bip122".to_string(), "000000000019d6689c085ae165831e93".to_string()), Some(Chain::Bitcoin) @@ -135,6 +143,7 @@ mod tests { ); 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_eq!(WalletConnectCAIP2::resolve_chain(Some("tron:0x2b6653dc".to_string())), Ok(Chain::Tron)); assert_eq!( WalletConnectCAIP2::resolve_chain(Some("bip122:000000000019d6689c085ae165831e93".to_string())), Ok(Chain::Bitcoin) diff --git a/crates/primitives/src/wallet_connector.rs b/crates/primitives/src/wallet_connector.rs index 2d3c8d6ac..81f4f84e4 100644 --- a/crates/primitives/src/wallet_connector.rs +++ b/crates/primitives/src/wallet_connector.rs @@ -57,6 +57,12 @@ pub enum WalletConnectionMethods { TonSendMessage, #[serde(rename = "ton_signData")] TonSignData, + #[serde(rename = "tron_signMessage")] + TronSignMessage, + #[serde(rename = "tron_signTransaction")] + TronSignTransaction, + #[serde(rename = "tron_sendTransaction")] + TronSendTransaction, #[serde(rename = "sendTransfer")] BtcSendTransfer, #[serde(rename = "signMessage")] diff --git a/gemstone/src/config/wallet_connect.rs b/gemstone/src/config/wallet_connect.rs index 91cb794c3..45a280efc 100644 --- a/gemstone/src/config/wallet_connect.rs +++ b/gemstone/src/config/wallet_connect.rs @@ -7,7 +7,7 @@ pub struct WalletConnectConfig { pub fn get_wallet_connect_config() -> WalletConnectConfig { let chains: Vec = [ - vec![Chain::Bitcoin, Chain::Solana, Chain::Sui, Chain::Ton], + vec![Chain::Bitcoin, Chain::Solana, Chain::Sui, Chain::Ton, Chain::Tron], EVMChain::all().iter().map(|x| x.to_chain()).collect(), ] .concat(); diff --git a/gemstone/src/message/sign_type.rs b/gemstone/src/message/sign_type.rs index 2ef6d0b44..33599414b 100644 --- a/gemstone/src/message/sign_type.rs +++ b/gemstone/src/message/sign_type.rs @@ -9,6 +9,7 @@ pub enum SignDigestType { Siwe, TonPersonal, BitcoinPersonal, + TronPersonal, } #[derive(Debug, uniffi::Record)] diff --git a/gemstone/src/message/signer.rs b/gemstone/src/message/signer.rs index 4840aa90e..dd9c05693 100644 --- a/gemstone/src/message/signer.rs +++ b/gemstone/src/message/signer.rs @@ -15,6 +15,7 @@ use super::{ }; use crate::{GemstoneError, siwe::SiweMessage}; use gem_bitcoin::signer::{BitcoinSignMessageData, sign_personal as bitcoin_sign_personal}; +use gem_tron::signer::tron_hash_message; use zeroize::Zeroizing; const SIGNATURE_LENGTH: usize = 65; @@ -42,7 +43,7 @@ impl MessageSigner { pub fn preview(&self) -> Result { match self.message.sign_type { - SignDigestType::SuiPersonal | SignDigestType::Eip191 => { + SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::TronPersonal => { let string = String::from_utf8(self.message.data.clone()); let preview = string.unwrap_or(encode_prefixed(&self.message.data)); Ok(MessagePreview::Text(preview)) @@ -89,7 +90,12 @@ impl MessageSigner { pub fn plain_preview(&self) -> String { match self.message.sign_type { - SignDigestType::SuiPersonal | SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::TonPersonal | SignDigestType::BitcoinPersonal => match self.preview() { + SignDigestType::SuiPersonal + | SignDigestType::Eip191 + | SignDigestType::TronPersonal + | SignDigestType::Base58 + | SignDigestType::TonPersonal + | SignDigestType::BitcoinPersonal => match self.preview() { Ok(MessagePreview::Text(preview)) => preview, _ => "".to_string(), }, @@ -112,6 +118,7 @@ impl MessageSigner { let ton_data = TonSignMessageData::from_bytes(string.as_bytes())?; Ok(ton_data.payload.hash()) } + SignDigestType::TronPersonal => Ok(tron_hash_message(&self.message.data).to_vec()), SignDigestType::Eip191 | SignDigestType::Siwe => Ok(eip191_hash_message(&self.message.data).to_vec()), SignDigestType::Eip712 => { let json = String::from_utf8(self.message.data.clone())?; @@ -128,7 +135,7 @@ impl MessageSigner { pub fn get_result(&self, data: &[u8]) -> String { match &self.message.sign_type { - SignDigestType::Eip191 | SignDigestType::Eip712 | SignDigestType::Siwe => { + SignDigestType::Eip191 | SignDigestType::Eip712 | SignDigestType::Siwe | SignDigestType::TronPersonal => { if data.len() < SIGNATURE_LENGTH { return encode_prefixed(data); } @@ -164,7 +171,7 @@ impl MessageSigner { 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 => { + SignDigestType::Eip191 | SignDigestType::Eip712 | SignDigestType::Siwe | SignDigestType::TronPersonal => { let signed = Signer::sign_digest(SignatureScheme::Secp256k1, hash, private_key.to_vec())?; Ok(self.get_result(&signed)) } @@ -177,6 +184,7 @@ impl MessageSigner { } } + #[cfg(test)] mod tests { use super::*; diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index c4807f758..dd4fc5cce 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -422,6 +422,7 @@ pub enum GemTransactionLoadMetadata { parent_hash: String, witness_address: String, votes: HashMap, + raw_data_hex: Option, }, Sui { message_bytes: String, @@ -504,6 +505,7 @@ impl From for GemTransactionLoadMetadata { parent_hash, witness_address, votes, + raw_data_hex, } => GemTransactionLoadMetadata::Tron { block_number, block_version, @@ -512,6 +514,7 @@ impl From for GemTransactionLoadMetadata { parent_hash, witness_address, votes, + raw_data_hex, }, TransactionLoadMetadata::Sui { message_bytes } => GemTransactionLoadMetadata::Sui { message_bytes }, TransactionLoadMetadata::Hyperliquid { order } => GemTransactionLoadMetadata::Hyperliquid { order }, @@ -592,6 +595,7 @@ impl From for TransactionLoadMetadata { parent_hash, witness_address, votes, + raw_data_hex, } => TransactionLoadMetadata::Tron { block_number, block_version, @@ -600,6 +604,7 @@ impl From for TransactionLoadMetadata { parent_hash, witness_address, votes, + raw_data_hex, }, GemTransactionLoadMetadata::Sui { message_bytes } => TransactionLoadMetadata::Sui { message_bytes }, GemTransactionLoadMetadata::Hyperliquid { order } => TransactionLoadMetadata::Hyperliquid { order }, diff --git a/gemstone/src/signer/chain.rs b/gemstone/src/signer/chain.rs index 90070f7f0..5d55626bb 100644 --- a/gemstone/src/signer/chain.rs +++ b/gemstone/src/signer/chain.rs @@ -1,6 +1,7 @@ use crate::{GemstoneError, models::transaction::GemTransactionLoadInput}; use gem_aptos::AptosChainSigner; use gem_hypercore::signer::HyperCoreSigner; +use gem_tron::TronChainSigner; use gem_sui::signer::SuiChainSigner; use primitives::{Chain, ChainSigner, SignerError, TransactionLoadInput}; @@ -18,6 +19,7 @@ impl GemChainSigner { Chain::Aptos => Box::new(AptosChainSigner), Chain::HyperCore => Box::new(HyperCoreSigner), Chain::Sui => Box::new(SuiChainSigner), + Chain::Tron => Box::new(TronChainSigner), _ => todo!("Signer not implemented for chain {:?}", chain), }; diff --git a/gemstone/src/siwe.rs b/gemstone/src/siwe.rs index 7360dcdf5..8b6d7f7e1 100644 --- a/gemstone/src/siwe.rs +++ b/gemstone/src/siwe.rs @@ -1,4 +1,5 @@ pub use gem_evm::siwe::SiweMessage; +use crate::GemstoneError; use primitives::Chain; #[uniffi::remote(Record)] @@ -19,5 +20,5 @@ pub fn siwe_try_parse(raw: String) -> Option { #[uniffi::export] pub fn siwe_validate(message: SiweMessage, chain: Chain) -> Result<(), crate::GemstoneError> { - message.validate(chain).map_err(|e| crate::GemstoneError::AnyError { msg: e }) + message.validate(chain).map_err(|e| GemstoneError::AnyError { msg: e }) } diff --git a/gemstone/src/wallet_connect/actions.rs b/gemstone/src/wallet_connect/actions.rs index c868f6ac0..c52b505e1 100644 --- a/gemstone/src/wallet_connect/actions.rs +++ b/gemstone/src/wallet_connect/actions.rs @@ -59,6 +59,7 @@ pub enum WalletConnectTransactionType { Sui { output_type: TransferDataOutputType }, Ton { output_type: TransferDataOutputType }, Bitcoin { output_type: TransferDataOutputType }, + Tron { output_type: TransferDataOutputType }, } #[derive(Debug, Clone, uniffi::Enum)] @@ -83,6 +84,10 @@ pub enum WalletConnectTransaction { data: String, output_type: TransferDataOutputType, }, + Tron { + data: String, + 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 af6ae42e8..78169860e 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -8,8 +8,7 @@ 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; +use crate::{GemstoneError, message::sign_type::{SignDigestType, SignMessage}, siwe::SiweMessage}; pub mod actions; pub mod handler_traits; @@ -168,7 +167,7 @@ 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, @@ -176,7 +175,7 @@ impl WalletConnect { chain_id: Some(chain_id), domain, }; - WalletConnectRequestHandler::parse_request(request).map_err(|e| crate::GemstoneError::AnyError { msg: e }) + WalletConnectRequestHandler::parse_request(request).map_err(|e| GemstoneError::AnyError { msg: e }) } pub fn validate_origin(&self, metadata_url: String, origin: Option, validation: WalletConnectionVerificationStatus) -> WalletConnectionVerificationStatus { @@ -195,33 +194,38 @@ impl WalletConnect { WalletConnectResponseHandler::encode_send_transaction(chain.chain_type(), transaction_id) } - pub fn validate_sign_message(&self, chain: Chain, sign_type: SignDigestType, data: String) -> Result<(), crate::GemstoneError> { + pub fn validate_sign_message(&self, chain: Chain, sign_type: SignDigestType, data: String) -> Result<(), GemstoneError> { match sign_type { SignDigestType::Eip712 => { - let expected_chain_id = chain.network_id().parse::().map_err(|_| crate::GemstoneError::AnyError { + let expected_chain_id = chain.network_id().parse::().map_err(|_| GemstoneError::AnyError { msg: format!("Chain {} does not have a numeric network ID", chain), })?; - gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| crate::GemstoneError::AnyError { msg: e }) + gem_evm::eip712::validate_eip712_chain_id(&data, expected_chain_id).map_err(|e| GemstoneError::AnyError { msg: e }) } SignDigestType::TonPersonal => { gem_ton::signer::TonSignMessageData::from_bytes(data.as_bytes())?; Ok(()) } - SignDigestType::Eip191 | SignDigestType::Base58 | SignDigestType::SuiPersonal | SignDigestType::Siwe | SignDigestType::BitcoinPersonal => Ok(()), + SignDigestType::Eip191 + | SignDigestType::Base58 + | SignDigestType::SuiPersonal + | SignDigestType::Siwe + | SignDigestType::BitcoinPersonal + | SignDigestType::TronPersonal => Ok(()), } } - pub fn validate_send_transaction(&self, transaction_type: WalletConnectTransactionType, data: String) -> Result<(), crate::GemstoneError> { + pub fn validate_send_transaction(&self, transaction_type: WalletConnectTransactionType, data: String) -> Result<(), 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() })?; + let json: serde_json::Value = serde_json::from_str(&data).map_err(|_| 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 { + return Err(GemstoneError::AnyError { msg: "Transaction expired".to_string(), }); } @@ -264,7 +268,7 @@ impl WalletConnect { }) } - pub fn decode_send_transaction(&self, transaction_type: WalletConnectTransactionType, data: String) -> Result { + pub fn decode_send_transaction(&self, transaction_type: WalletConnectTransactionType, data: String) -> Result { match transaction_type { WalletConnectTransactionType::Ethereum => { let tx: WCEthereumTransaction = serde_json::from_str(&data)?; @@ -276,7 +280,7 @@ impl WalletConnect { let transaction = json .get("transaction") .and_then(|v| v.as_str()) - .ok_or_else(|| crate::GemstoneError::AnyError { + .ok_or_else(|| GemstoneError::AnyError { msg: "Missing transaction field".to_string(), })? .to_string(); @@ -292,7 +296,7 @@ impl WalletConnect { let transaction = json .get("transaction") .and_then(|v| v.as_str()) - .ok_or_else(|| crate::GemstoneError::AnyError { + .ok_or_else(|| GemstoneError::AnyError { msg: "Missing transaction field".to_string(), })? .to_string(); @@ -309,7 +313,7 @@ impl WalletConnect { let messages = json .get("messages") - .ok_or_else(|| crate::GemstoneError::AnyError { + .ok_or_else(|| GemstoneError::AnyError { msg: "Missing messages field".to_string(), })? .to_string(); @@ -317,6 +321,18 @@ impl WalletConnect { Ok(WalletConnectTransaction::Ton { messages, output_type }) } WalletConnectTransactionType::Bitcoin { output_type } => Ok(WalletConnectTransaction::Bitcoin { data, output_type }), + WalletConnectTransactionType::Tron { output_type } => { + let json: serde_json::Value = serde_json::from_str(&data)?; + + let transaction = json + .get("transaction") + .ok_or_else(|| GemstoneError::AnyError { + msg: "Missing transaction field".to_string(), + })? + .to_string(); + + Ok(WalletConnectTransaction::Tron { data: transaction, output_type }) + } } } } diff --git a/gemstone/src/wallet_connect/request_handler/mod.rs b/gemstone/src/wallet_connect/request_handler/mod.rs index 947f78a9a..23924049b 100644 --- a/gemstone/src/wallet_connect/request_handler/mod.rs +++ b/gemstone/src/wallet_connect/request_handler/mod.rs @@ -3,6 +3,7 @@ mod ethereum; mod solana; mod sui; mod ton; +mod tron; use crate::wallet_connect::actions::{WalletConnectAction, WalletConnectChainOperation}; use crate::wallet_connect::handler_traits::ChainRequestHandler; @@ -13,6 +14,7 @@ use serde_json::Value; use solana::SolanaRequestHandler; use sui::SuiRequestHandler; use ton::TonRequestHandler; +use tron::TronRequestHandler; pub struct WalletConnectRequestHandler; @@ -60,6 +62,9 @@ impl WalletConnectRequestHandler { WalletConnectionMethods::SuiSignAndExecuteTransaction => SuiRequestHandler::parse_send_transaction(Chain::Sui, params), WalletConnectionMethods::TonSignData => TonRequestHandler::parse_sign_message(Chain::Ton, params, domain), WalletConnectionMethods::TonSendMessage => TonRequestHandler::parse_send_transaction(Chain::Ton, params), + WalletConnectionMethods::TronSignMessage => TronRequestHandler::parse_sign_message(Chain::Tron, params, domain), + WalletConnectionMethods::TronSignTransaction => TronRequestHandler::parse_sign_transaction(Chain::Tron, params), + WalletConnectionMethods::TronSendTransaction => TronRequestHandler::parse_send_transaction(Chain::Tron, params), WalletConnectionMethods::BtcSignMessage => BitcoinRequestHandler::parse_sign_message(Chain::Bitcoin, params, domain), WalletConnectionMethods::BtcSendTransfer => BitcoinRequestHandler::parse_send_transaction(Chain::Bitcoin, params), } diff --git a/gemstone/src/wallet_connect/request_handler/tron.rs b/gemstone/src/wallet_connect/request_handler/tron.rs new file mode 100644 index 000000000..4a2fecc14 --- /dev/null +++ b/gemstone/src/wallet_connect/request_handler/tron.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 TronRequestHandler; + +impl ChainRequestHandler for TronRequestHandler { + 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 { + chain: Chain::Tron, + sign_type: SignDigestType::TronPersonal, + data: message, + }) + } + + fn parse_sign_transaction(_chain: Chain, params: Value) -> Result { + if params.get("transaction").is_none() && params.get("raw_data_hex").is_none() { + return Err("Missing transaction parameter".to_string()); + } + + Ok(WalletConnectAction::SignTransaction { + chain: Chain::Tron, + transaction_type: WalletConnectTransactionType::Tron { + output_type: TransferDataOutputType::EncodedTransaction, + }, + data: params.to_string(), + }) + } + + fn parse_send_transaction(_chain: Chain, params: Value) -> Result { + if params.get("transaction").is_none() && params.get("raw_data_hex").is_none() { + return Err("Missing transaction parameter".to_string()); + } + + Ok(WalletConnectAction::SendTransaction { + chain: Chain::Tron, + transaction_type: WalletConnectTransactionType::Tron { + 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#"{"message":"Hello"}"#).unwrap(); + let action = TronRequestHandler::parse_sign_message(Chain::Tron, params, "example.com").unwrap(); + let WalletConnectAction::SignMessage { chain, sign_type, data } = action else { + panic!("Expected SignMessage action") + }; + assert_eq!(chain, Chain::Tron); + assert_eq!(sign_type, SignDigestType::TronPersonal); + assert_eq!(data, "Hello"); + } + + #[test] + fn test_parse_sign_transaction() { + let params = serde_json::from_str(r#"{"transaction":{"raw_data_hex":"abc"}}"#).unwrap(); + let action = TronRequestHandler::parse_sign_transaction(Chain::Tron, params).unwrap(); + let WalletConnectAction::SignTransaction { chain, transaction_type, data } = action else { + panic!("Expected SignTransaction action") + }; + assert_eq!(chain, Chain::Tron); + let WalletConnectTransactionType::Tron { + output_type: TransferDataOutputType::EncodedTransaction, + } = transaction_type + else { + panic!("Expected Tron 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("transaction").is_some()); + } + + #[test] + fn test_parse_sign_transaction_with_raw_data_hex() { + let params = serde_json::from_str(r#"{"raw_data_hex":"abc"}"#).unwrap(); + let action = TronRequestHandler::parse_sign_transaction(Chain::Tron, params).unwrap(); + let WalletConnectAction::SignTransaction { chain, transaction_type, data } = action else { + panic!("Expected SignTransaction action") + }; + assert_eq!(chain, Chain::Tron); + let WalletConnectTransactionType::Tron { + output_type: TransferDataOutputType::EncodedTransaction, + } = transaction_type + else { + panic!("Expected Tron 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("raw_data_hex").is_some()); + } +} diff --git a/gemstone/src/wallet_connect/response_handler.rs b/gemstone/src/wallet_connect/response_handler.rs index 422689a86..4430b9610 100644 --- a/gemstone/src/wallet_connect/response_handler.rs +++ b/gemstone/src/wallet_connect/response_handler.rs @@ -26,7 +26,7 @@ impl ChainResponseHandler for WalletConnectResponseHandler { impl WalletConnectResponseHandler { pub fn encode_sign_message(chain_type: ChainType, signature: String) -> WalletConnectResponseType { match chain_type { - ChainType::Solana | ChainType::Sui => { + ChainType::Solana | ChainType::Sui | ChainType::Tron => { let result = serde_json::json!({ "signature": signature }); @@ -59,6 +59,7 @@ impl WalletConnectResponseHandler { }; WalletConnectResponseType::Object { json: result.to_string() } } + ChainType::Tron => WalletConnectResponseType::Object { json: transaction_id }, _ => WalletConnectResponseType::String { value: transaction_id }, } } @@ -110,6 +111,18 @@ mod tests { } } + #[test] + fn test_encode_sign_message_tron() { + let result = WalletConnectResponseHandler::encode_sign_message(ChainType::Tron, "tronsig123".to_string()); + match result { + WalletConnectResponseType::Object { json } => { + assert!(json.contains("\"signature\"")); + assert!(json.contains("tronsig123")); + } + _ => panic!("Expected Object response for Tron"), + } + } + #[test] fn test_encode_sign_transaction_ethereum() { let result = WalletConnectResponseHandler::encode_sign_transaction(ChainType::Ethereum, "0xtxid".to_string()); @@ -119,6 +132,16 @@ mod tests { assert_eq!(value, "0xtxid"); } + #[test] + fn test_encode_sign_transaction_tron() { + let json = r#"{"signature":["sig"]}"#.to_string(); + let result = WalletConnectResponseHandler::encode_sign_transaction(ChainType::Tron, json.clone()); + let WalletConnectResponseType::Object { json: result_json } = result else { + panic!("Expected Object response for Tron") + }; + assert_eq!(result_json, json); + } + #[test] fn test_encode_sign_transaction_solana() { let result = WalletConnectResponseHandler::encode_sign_transaction(ChainType::Solana, "txid123".to_string()); From b452c557f90cc8c8ef00f1f824493262be7648e7 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:21:38 +0900 Subject: [PATCH 2/8] code cleanup --- crates/gem_tron/src/signer/chain_signer.rs | 42 ++++++++++++---------- gemstone/src/models/transaction.rs | 16 ++++----- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/crates/gem_tron/src/signer/chain_signer.rs b/crates/gem_tron/src/signer/chain_signer.rs index 21f19a7f2..8d65d9446 100644 --- a/crates/gem_tron/src/signer/chain_signer.rs +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -40,12 +40,11 @@ impl ChainSigner for TronChainSigner { fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result { let (transaction, metadata) = extract_transaction(input)?; if !transaction.signature.is_empty() { - return Err(SignerError::InvalidInput("Tron multisig not supported for WalletConnect signing".to_string())); + return Err(invalid_input("Tron multisig not supported for WalletConnect signing")); } let raw_bytes = decode_hex(&metadata.raw_data_hex)?; let digest = Sha256::digest(&raw_bytes); - let signature = Signer::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key.to_vec()) - .map_err(|err| SignerError::InvalidInput(err.to_string()))?; + let signature = sign_digest(&digest, private_key)?; let signature_hex = hex::encode(signature); match metadata.output_type { @@ -55,7 +54,7 @@ fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result { extract_transaction_payload(&payload) - .ok_or_else(|| SignerError::InvalidInput("Missing transaction object for Tron broadcast".to_string()))? + .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))? } TransferDataOutputAction::Sign => payload, }; @@ -67,19 +66,17 @@ fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result Result<(TronTransaction, PayloadMetadata), SignerError> { let TransactionInputType::Generic(_, _, extra) = &input.input_type else { - return Err(SignerError::InvalidInput("Expected generic transaction input".to_string())); + return Err(invalid_input("Expected generic transaction input")); }; - let data = extra.data.as_ref().ok_or_else(|| SignerError::InvalidInput("Missing transaction data".to_string()))?; + let data = extra.data.as_ref().ok_or_else(|| invalid_input("Missing transaction data"))?; - let mut payload: Value = serde_json::from_slice(data)?; - if let Value::String(raw_json) = &payload - && let Ok(parsed) = serde_json::from_str::(raw_json) - { - payload = parsed; - } + let payload = match serde_json::from_slice::(data)? { + Value::String(raw_json) => serde_json::from_str::(&raw_json).unwrap_or(Value::String(raw_json)), + value => value, + }; let transaction_value = extract_transaction_payload(&payload) - .ok_or_else(|| SignerError::InvalidInput("Missing transaction object for Tron broadcast".to_string()))?; + .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))?; let transaction: TronTransaction = serde_json::from_value(transaction_value)?; let raw_data_hex = match &input.metadata { TransactionLoadMetadata::Tron { @@ -89,7 +86,7 @@ fn extract_transaction(input: &TransactionLoadInput) -> Result<(TronTransaction, _ => transaction .raw_data_hex .clone() - .ok_or_else(|| SignerError::InvalidInput("Missing raw_data_hex in Tron transaction payload".to_string()))?, + .ok_or_else(|| invalid_input("Missing raw_data_hex in Tron transaction payload"))?, }; let metadata = PayloadMetadata { @@ -101,6 +98,11 @@ fn extract_transaction(input: &TransactionLoadInput) -> Result<(TronTransaction, Ok((transaction, metadata)) } +fn sign_digest(digest: &[u8], private_key: &[u8]) -> Result, SignerError> { + Signer::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key.to_vec()) + .map_err(|err| invalid_input(err.to_string())) +} + fn extract_transaction_payload(value: &Value) -> Option { match value { Value::Object(map) => { @@ -121,10 +123,10 @@ fn extract_transaction_payload(value: &Value) -> Option { fn apply_signature(payload: Value, signature_hex: &str) -> Result { let transaction_value = extract_transaction_payload(&payload) - .ok_or_else(|| SignerError::InvalidInput("Missing transaction object for Tron broadcast".to_string()))?; + .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))?; let mut transaction: TronTransaction = serde_json::from_value(transaction_value)?; if !transaction.signature.is_empty() { - return Err(SignerError::InvalidInput("Tron multisig not supported for WalletConnect signing".to_string())); + return Err(invalid_input("Tron multisig not supported for WalletConnect signing")); } transaction.signature = vec![signature_hex.to_string()]; let updated_transaction = serde_json::to_value(transaction)?; @@ -136,9 +138,13 @@ fn apply_signature(payload: Value, signature_hex: &str) -> Result Err(SignerError::InvalidInput("Invalid Tron transaction payload".to_string())), + _ => Err(invalid_input("Invalid Tron transaction payload")), } } + +fn invalid_input(message: impl Into) -> SignerError { + SignerError::InvalidInput(message.into()) +} diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 85d9fc0c2..30f40b3b7 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -144,6 +144,14 @@ pub struct GemTransactionStateRequest { pub block_number: i64, } +pub type GemStakeData = StakeData; + +#[uniffi::remote(Record)] +pub struct GemStakeData { + pub data: Option, + pub to: Option, +} + pub type GemHyperliquidOrder = HyperliquidOrder; #[uniffi::remote(Record)] @@ -156,14 +164,6 @@ pub struct GemHyperliquidOrder { pub agent_private_key: String, } -pub type GemStakeData = StakeData; - -#[uniffi::remote(Record)] -pub struct GemStakeData { - pub data: Option, - pub to: Option, -} - #[derive(Debug, Clone, uniffi::Enum)] pub enum GemStakeType { Delegate { validator: GemDelegationValidator }, From 3fe7eacdebb76e356bc496f25e737fc02898e8a5 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:44:34 +0900 Subject: [PATCH 3/8] =?UTF-8?q?impl=20tron=5Fmethod=5Fversion:=20=E2=80=9C?= =?UTF-8?q?v1=E2=80=9D=20and=20add=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/gem_tron/src/signer/chain_signer.rs | 53 ++--- gemstone/src/wallet_connect/mod.rs | 183 +++++++++++++++++- .../src/wallet_connect/request_handler/mod.rs | 4 + .../wallet_connect/request_handler/tron.rs | 17 +- .../mod.rs} | 25 ++- .../wallet_connect/response_handler/tron.rs | 33 ++++ .../test/tron_send_transaction.json | 28 +++ .../test/tron_sign_message.json | 4 + .../test/tron_sign_message_response.json | 1 + .../test/tron_sign_transaction.json | 28 +++ .../test/tron_sign_transaction_response.json | 1 + 11 files changed, 323 insertions(+), 54 deletions(-) rename gemstone/src/wallet_connect/{response_handler.rs => response_handler/mod.rs} (89%) create mode 100644 gemstone/src/wallet_connect/response_handler/tron.rs create mode 100644 gemstone/src/wallet_connect/test/tron_send_transaction.json create mode 100644 gemstone/src/wallet_connect/test/tron_sign_message.json create mode 100644 gemstone/src/wallet_connect/test/tron_sign_message_response.json create mode 100644 gemstone/src/wallet_connect/test/tron_sign_transaction.json create mode 100644 gemstone/src/wallet_connect/test/tron_sign_transaction_response.json diff --git a/crates/gem_tron/src/signer/chain_signer.rs b/crates/gem_tron/src/signer/chain_signer.rs index 8d65d9446..a040fa5ac 100644 --- a/crates/gem_tron/src/signer/chain_signer.rs +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -52,10 +52,8 @@ fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result { let payload = apply_signature(metadata.payload, &signature_hex)?; let result_payload = match metadata.output_action { - TransferDataOutputAction::Send => { - extract_transaction_payload(&payload) - .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))? - } + TransferDataOutputAction::Send => extract_transaction_value(&payload) + .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))?, TransferDataOutputAction::Sign => payload, }; @@ -75,8 +73,10 @@ fn extract_transaction(input: &TransactionLoadInput) -> Result<(TronTransaction, value => value, }; - let transaction_value = extract_transaction_payload(&payload) - .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))?; + let Value::Object(map) = &payload else { + return Err(invalid_input("Invalid Tron transaction payload")); + }; + let transaction_value = map.get("transaction").cloned().ok_or_else(|| invalid_input("Missing transaction field"))?; let transaction: TronTransaction = serde_json::from_value(transaction_value)?; let raw_data_hex = match &input.metadata { TransactionLoadMetadata::Tron { @@ -103,46 +103,27 @@ fn sign_digest(digest: &[u8], private_key: &[u8]) -> Result, SignerError .map_err(|err| invalid_input(err.to_string())) } -fn extract_transaction_payload(value: &Value) -> Option { - match value { - Value::Object(map) => { - if map.get("raw_data_hex").is_some() { - return Some(value.clone()); - } - if let Some(transaction) = map.get("transaction") - && let Some(found) = extract_transaction_payload(transaction) - { - return Some(found); - } - map.values().find_map(extract_transaction_payload) - } - Value::Array(values) => values.iter().find_map(extract_transaction_payload), +fn extract_transaction_value(payload: &Value) -> Option { + match payload { + Value::Object(map) => map.get("transaction").cloned(), _ => None, } } fn apply_signature(payload: Value, signature_hex: &str) -> Result { - let transaction_value = extract_transaction_payload(&payload) - .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))?; + let Value::Object(mut map) = payload else { + return Err(invalid_input("Invalid Tron transaction payload")); + }; + let transaction_value = map.get("transaction").cloned().ok_or_else(|| invalid_input("Missing transaction field"))?; let mut transaction: TronTransaction = serde_json::from_value(transaction_value)?; if !transaction.signature.is_empty() { return Err(invalid_input("Tron multisig not supported for WalletConnect signing")); } transaction.signature = vec![signature_hex.to_string()]; - let updated_transaction = serde_json::to_value(transaction)?; - match payload { - Value::Object(mut map) => { - if map.get("raw_data_hex").is_some() { - Ok(updated_transaction) - } else if map.get("transaction").is_some() { - map.insert("transaction".to_string(), updated_transaction); - Ok(Value::Object(map)) - } else { - Err(invalid_input("Missing raw_data_hex in Tron transaction payload")) - } - } - _ => Err(invalid_input("Invalid Tron transaction payload")), - } + map.insert("transaction".to_string(), serde_json::to_value(transaction)?); + map.entry("signature".to_string()) + .or_insert(serde_json::Value::String(signature_hex.to_string())); + Ok(Value::Object(map)) } fn invalid_input(message: impl Into) -> SignerError { diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index 78169860e..9eeb13fed 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -37,6 +37,22 @@ impl Default for WalletConnect { #[cfg(test)] mod tests { use super::*; + use crate::message::signer::MessageSigner; + use gem_tron::TronChainSigner; + use primitives::{ + Asset, ChainSigner, GasPriceType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransferDataExtra, TransferDataOutputAction, + TransferDataOutputType, TronStakeData, WalletConnectionSessionAppMetadata, + }; + + fn make_request(method: &str, params: &str, chain_id: Option<&str>) -> WalletConnectRequest { + WalletConnectRequest { + topic: "test-topic".to_string(), + method: method.to_string(), + params: params.to_string(), + chain_id: chain_id.map(|value| value.to_string()), + domain: "example.com".to_string(), + } + } fn sample_siwe_message() -> String { [ @@ -125,6 +141,161 @@ mod tests { ); } + #[test] + fn parse_tron_sign_message_and_sign() { + let wallet_connect = WalletConnect::new(); + let params = include_str!("./test/tron_sign_message.json"); + let raw_params = serde_json::to_string(¶ms.trim()).unwrap(); + let request = make_request("tron_signMessage", &raw_params, Some("tron:0x2b6653dc")); + + let action = WalletConnectRequestHandler::parse_request(request).unwrap(); + let WalletConnectAction::SignMessage { chain, sign_type, data } = action else { + panic!("Expected SignMessage action"); + }; + + assert_eq!(chain, Chain::Tron); + assert_eq!(sign_type, SignDigestType::TronPersonal); + assert_eq!(data, "This is a message to be signed for Tron"); + + let sign_message = wallet_connect.decode_sign_message(chain, sign_type, data); + let signer = MessageSigner::new(sign_message); + let signature = signer.sign(vec![1u8; 32]).unwrap(); + + assert_eq!( + signature, + "0xa0cbc20e8f0a9c19dd3d97e15fd99eee49edb8c0bcca52b684bbf13e1344b99670201d57633881cb20b0c00b626397530e3165049044b2fa4089840cf41a0a761b" + ); + + let response = wallet_connect.encode_sign_message(chain, signature.clone()); + match response { + WalletConnectResponseType::Object { json } => { + let expected_json = include_str!("./test/tron_sign_message_response.json"); + assert_eq!(json, expected_json.trim()); + } + _ => panic!("Expected Object response for Tron"), + } + } + + #[test] + fn parse_tron_sign_transaction_and_sign() { + let wallet_connect = WalletConnect::new(); + let params = include_str!("./test/tron_sign_transaction.json"); + let request = make_request( + "tron_signTransaction", + &serde_json::to_string(¶ms.trim()).unwrap(), + Some("tron:0x2b6653dc"), + ); + + let action = WalletConnectRequestHandler::parse_request(request).unwrap(); + let WalletConnectAction::SignTransaction { + chain, + transaction_type, + data, + } = action + else { + panic!("Expected SignTransaction action"); + }; + + assert_eq!(chain, Chain::Tron); + let WalletConnectTransactionType::Tron { + output_type: TransferDataOutputType::EncodedTransaction, + } = transaction_type + else { + panic!("Expected Tron transaction type with EncodedTransaction output"); + }; + + let input = TransactionLoadInput { + input_type: TransactionInputType::Generic( + Asset::from_chain(Chain::Tron), + WalletConnectionSessionAppMetadata { + name: "Test Dapp".to_string(), + description: "Test Dapp".to_string(), + url: "https://example.com".to_string(), + icon: "https://example.com/icon.png".to_string(), + }, + TransferDataExtra { + to: "".to_string(), + gas_limit: None, + gas_price: None, + data: Some(data.as_bytes().to_vec()), + output_type: TransferDataOutputType::EncodedTransaction, + output_action: TransferDataOutputAction::Sign, + }, + ), + sender_address: "TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM".to_string(), + destination_address: "".to_string(), + value: "0".to_string(), + gas_price: GasPriceType::regular(0), + memo: None, + is_max_value: false, + metadata: TransactionLoadMetadata::Tron { + block_number: 0, + block_version: 0, + block_timestamp: 0, + transaction_tree_root: "".to_string(), + parent_hash: "".to_string(), + witness_address: "".to_string(), + stake_data: TronStakeData::Votes(vec![]), + raw_data_hex: None, + }, + }; + + let signature_payload = TronChainSigner.sign_data(&input, &[1u8; 32]).unwrap(); + let value: serde_json::Value = serde_json::from_str(&signature_payload).unwrap(); + let signature = value + .get("transaction") + .and_then(|v| v.get("signature")) + .and_then(|v| v.get(0)) + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(); + + assert_eq!( + signature, + "943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00" + ); + + let response = wallet_connect.encode_sign_transaction(chain, signature_payload.clone()); + match response { + WalletConnectResponseType::Object { json } => { + let expected_json = include_str!("./test/tron_sign_transaction_response.json"); + assert_eq!(json, expected_json.trim()); + } + _ => panic!("Expected Object response for Tron"), + } + } + + #[test] + fn parse_tron_send_transaction() { + let params = include_str!("./test/tron_send_transaction.json"); + let request = make_request( + "tron_sendTransaction", + &serde_json::to_string(¶ms.trim()).unwrap(), + Some("tron:0x2b6653dc"), + ); + + let action = WalletConnectRequestHandler::parse_request(request).unwrap(); + let WalletConnectAction::SendTransaction { + chain, + transaction_type, + data, + } = action + else { + panic!("Expected SendTransaction action"); + }; + + assert_eq!(chain, Chain::Tron); + let WalletConnectTransactionType::Tron { + output_type: TransferDataOutputType::EncodedTransaction, + } = transaction_type + else { + panic!("Expected Tron transaction type with EncodedTransaction output"); + }; + + let parsed_data: serde_json::Value = serde_json::from_str(&data).unwrap(); + assert!(parsed_data.get("transaction").is_some()); + } + #[test] fn validate_ton_send_transaction() { let wallet_connect = WalletConnect::new(); @@ -323,15 +494,13 @@ impl WalletConnect { WalletConnectTransactionType::Bitcoin { output_type } => Ok(WalletConnectTransaction::Bitcoin { data, output_type }), WalletConnectTransactionType::Tron { output_type } => { let json: serde_json::Value = serde_json::from_str(&data)?; - - let transaction = json - .get("transaction") - .ok_or_else(|| GemstoneError::AnyError { + if json.get("transaction").is_none() { + return Err(GemstoneError::AnyError { msg: "Missing transaction field".to_string(), - })? - .to_string(); + }); + } - Ok(WalletConnectTransaction::Tron { data: transaction, output_type }) + Ok(WalletConnectTransaction::Tron { data, output_type }) } } } diff --git a/gemstone/src/wallet_connect/request_handler/mod.rs b/gemstone/src/wallet_connect/request_handler/mod.rs index 23924049b..8e811608a 100644 --- a/gemstone/src/wallet_connect/request_handler/mod.rs +++ b/gemstone/src/wallet_connect/request_handler/mod.rs @@ -23,6 +23,10 @@ impl WalletConnectRequestHandler { let method = serde_json::from_value::(serde_json::Value::String(request.method.clone())).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 params = match params { + Value::String(raw_json) => serde_json::from_str::(&raw_json).unwrap_or(Value::String(raw_json)), + value => value, + }; let domain = &request.domain; diff --git a/gemstone/src/wallet_connect/request_handler/tron.rs b/gemstone/src/wallet_connect/request_handler/tron.rs index 4a2fecc14..ffcae329b 100644 --- a/gemstone/src/wallet_connect/request_handler/tron.rs +++ b/gemstone/src/wallet_connect/request_handler/tron.rs @@ -4,6 +4,7 @@ use crate::wallet_connect::handler_traits::ChainRequestHandler; use primitives::{Chain, TransferDataOutputType}; use serde_json::Value; +// https://docs.reown.com/advanced/multichain/rpc-reference/tron-rpc pub struct TronRequestHandler; impl ChainRequestHandler for TronRequestHandler { @@ -18,7 +19,7 @@ impl ChainRequestHandler for TronRequestHandler { } fn parse_sign_transaction(_chain: Chain, params: Value) -> Result { - if params.get("transaction").is_none() && params.get("raw_data_hex").is_none() { + if params.get("transaction").is_none() { return Err("Missing transaction parameter".to_string()); } @@ -32,7 +33,7 @@ impl ChainRequestHandler for TronRequestHandler { } fn parse_send_transaction(_chain: Chain, params: Value) -> Result { - if params.get("transaction").is_none() && params.get("raw_data_hex").is_none() { + if params.get("transaction").is_none() { return Err("Missing transaction parameter".to_string()); } @@ -81,11 +82,11 @@ mod tests { } #[test] - fn test_parse_sign_transaction_with_raw_data_hex() { - let params = serde_json::from_str(r#"{"raw_data_hex":"abc"}"#).unwrap(); - let action = TronRequestHandler::parse_sign_transaction(Chain::Tron, params).unwrap(); - let WalletConnectAction::SignTransaction { chain, transaction_type, data } = action else { - panic!("Expected SignTransaction action") + fn test_parse_send_transaction() { + let params = serde_json::from_str(r#"{"transaction":{"raw_data_hex":"abc"}}"#).unwrap(); + let action = TronRequestHandler::parse_send_transaction(Chain::Tron, params).unwrap(); + let WalletConnectAction::SendTransaction { chain, transaction_type, data } = action else { + panic!("Expected SendTransaction action") }; assert_eq!(chain, Chain::Tron); let WalletConnectTransactionType::Tron { @@ -95,6 +96,6 @@ mod tests { panic!("Expected Tron 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("raw_data_hex").is_some()); + assert!(parsed_data.get("transaction").is_some()); } } diff --git a/gemstone/src/wallet_connect/response_handler.rs b/gemstone/src/wallet_connect/response_handler/mod.rs similarity index 89% rename from gemstone/src/wallet_connect/response_handler.rs rename to gemstone/src/wallet_connect/response_handler/mod.rs index 4430b9610..b3c556efd 100644 --- a/gemstone/src/wallet_connect/response_handler.rs +++ b/gemstone/src/wallet_connect/response_handler/mod.rs @@ -1,6 +1,10 @@ use crate::wallet_connect::handler_traits::ChainResponseHandler; use primitives::ChainType; +mod tron; + +use tron::TronResponseHandler; + #[derive(Debug, Clone, uniffi::Enum)] pub enum WalletConnectResponseType { String { value: String }, @@ -26,7 +30,8 @@ impl ChainResponseHandler for WalletConnectResponseHandler { impl WalletConnectResponseHandler { pub fn encode_sign_message(chain_type: ChainType, signature: String) -> WalletConnectResponseType { match chain_type { - ChainType::Solana | ChainType::Sui | ChainType::Tron => { + ChainType::Tron => TronResponseHandler::encode_sign_message(signature), + ChainType::Solana | ChainType::Sui => { let result = serde_json::json!({ "signature": signature }); @@ -41,6 +46,7 @@ impl WalletConnectResponseHandler { pub fn encode_sign_transaction(chain_type: ChainType, transaction_id: String) -> WalletConnectResponseType { match chain_type { + ChainType::Tron => TronResponseHandler::encode_sign_transaction(transaction_id), ChainType::Solana | ChainType::Ton => WalletConnectResponseType::Object { json: serde_json::json!({ "signature": transaction_id }).to_string(), }, @@ -59,13 +65,13 @@ impl WalletConnectResponseHandler { }; WalletConnectResponseType::Object { json: result.to_string() } } - ChainType::Tron => WalletConnectResponseType::Object { json: transaction_id }, _ => WalletConnectResponseType::String { value: transaction_id }, } } pub fn encode_send_transaction(chain_type: ChainType, transaction_id: String) -> WalletConnectResponseType { match chain_type { + ChainType::Tron => TronResponseHandler::encode_send_transaction(transaction_id), ChainType::Sui => WalletConnectResponseType::Object { json: serde_json::json!({ "digest": transaction_id }).to_string(), }, @@ -139,7 +145,7 @@ mod tests { let WalletConnectResponseType::Object { json: result_json } = result else { panic!("Expected Object response for Tron") }; - assert_eq!(result_json, json); + assert_eq!(result_json, r#"{"signature":["sig"],"result":true}"#); } #[test] @@ -189,6 +195,19 @@ mod tests { } } + #[test] + fn test_encode_send_transaction_tron() { + let result = WalletConnectResponseHandler::encode_send_transaction(ChainType::Tron, "txid123".to_string()); + match result { + WalletConnectResponseType::Object { json } => { + assert!(json.contains("\"result\"")); + assert!(json.contains("\"txid\"")); + assert!(json.contains("txid123")); + } + _ => panic!("Expected Object response for Tron"), + } + } + #[test] fn test_encode_sign_message_ton() { let payload_json = r#"{"signature":"tonsig123","timestamp":1700000000}"#.to_string(); diff --git a/gemstone/src/wallet_connect/response_handler/tron.rs b/gemstone/src/wallet_connect/response_handler/tron.rs new file mode 100644 index 000000000..c1582d64a --- /dev/null +++ b/gemstone/src/wallet_connect/response_handler/tron.rs @@ -0,0 +1,33 @@ +use crate::wallet_connect::handler_traits::ChainResponseHandler; +use crate::wallet_connect::response_handler::WalletConnectResponseType; + +pub struct TronResponseHandler; + +impl ChainResponseHandler for TronResponseHandler { + fn encode_sign_message(signature: String) -> WalletConnectResponseType { + let result = serde_json::json!({ + "signature": signature + }); + WalletConnectResponseType::Object { + json: serde_json::to_string(&result).unwrap_or_default(), + } + } + + fn encode_sign_transaction(transaction_id: String) -> WalletConnectResponseType { + let mut value = + serde_json::from_str::(&transaction_id).unwrap_or_else(|_| serde_json::Value::String(transaction_id)); + if let serde_json::Value::Object(map) = &mut value { + map.entry("result".to_string()).or_insert(serde_json::Value::Bool(true)); + } + let json = match value { + serde_json::Value::String(value) => value, + _ => value.to_string(), + }; + WalletConnectResponseType::Object { json } + } + + fn encode_send_transaction(transaction_id: String) -> WalletConnectResponseType { + let json = serde_json::json!({ "result": true, "txid": transaction_id }).to_string(); + WalletConnectResponseType::Object { json } + } +} diff --git a/gemstone/src/wallet_connect/test/tron_send_transaction.json b/gemstone/src/wallet_connect/test/tron_send_transaction.json new file mode 100644 index 000000000..9beab1731 --- /dev/null +++ b/gemstone/src/wallet_connect/test/tron_send_transaction.json @@ -0,0 +1,28 @@ +{ + "address": "TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM", + "transaction": { + "raw_data": { + "contract": [ + { + "parameter": { + "type_url": "type.googleapis.com/protocol.TriggerSmartContract", + "value": { + "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", + "data": "095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000", + "owner_address": "4160e00625a95cbc180f290e2611c826f90eeba56f" + } + }, + "type": "TriggerSmartContract" + } + ], + "expiration": 1770271569000, + "fee_limit": 200000000, + "ref_block_bytes": "b435", + "ref_block_hash": "eb7b23d0ef96d04e", + "timestamp": 1770271511581 + }, + "raw_data_hex": "0a02b4352208eb7b23d0ef96d04e40e8e8a1e3c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000709da89ee3c23390018084af5f", + "txID": "0c195049c6eb9792017e1411604ef691c2a02725603edacb91721831fa85c4b2", + "visible": false + } +} diff --git a/gemstone/src/wallet_connect/test/tron_sign_message.json b/gemstone/src/wallet_connect/test/tron_sign_message.json new file mode 100644 index 000000000..21fd88c60 --- /dev/null +++ b/gemstone/src/wallet_connect/test/tron_sign_message.json @@ -0,0 +1,4 @@ +{ + "address": "TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM", + "message": "This is a message to be signed for Tron" +} \ No newline at end of file diff --git a/gemstone/src/wallet_connect/test/tron_sign_message_response.json b/gemstone/src/wallet_connect/test/tron_sign_message_response.json new file mode 100644 index 000000000..8cb9e8ced --- /dev/null +++ b/gemstone/src/wallet_connect/test/tron_sign_message_response.json @@ -0,0 +1 @@ +{"signature":"0xa0cbc20e8f0a9c19dd3d97e15fd99eee49edb8c0bcca52b684bbf13e1344b99670201d57633881cb20b0c00b626397530e3165049044b2fa4089840cf41a0a761b"} diff --git a/gemstone/src/wallet_connect/test/tron_sign_transaction.json b/gemstone/src/wallet_connect/test/tron_sign_transaction.json new file mode 100644 index 000000000..a12b93ed8 --- /dev/null +++ b/gemstone/src/wallet_connect/test/tron_sign_transaction.json @@ -0,0 +1,28 @@ +{ + "address": "TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM", + "transaction": { + "raw_data": { + "contract": [ + { + "parameter": { + "type_url": "type.googleapis.com/protocol.TriggerSmartContract", + "value": { + "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", + "data": "095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000", + "owner_address": "4160e00625a95cbc180f290e2611c826f90eeba56f" + } + }, + "type": "TriggerSmartContract" + } + ], + "expiration": 1770267837000, + "fee_limit": 200000000, + "ref_block_bytes": "af5b", + "ref_block_hash": "64a0e8e5926b22fc", + "timestamp": 1770267778282 + }, + "raw_data_hex": "0a02af5b220864a0e8e5926b22fc40c884bee1c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f000000000000000000000000000000000000000000000000000000000000000070eab9bae1c23390018084af5f", + "txID": "fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205", + "visible": false + } +} diff --git a/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json new file mode 100644 index 000000000..6c38547da --- /dev/null +++ b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json @@ -0,0 +1 @@ +{"address":"TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM","transaction":{"raw_data_hex":"0a02af5b220864a0e8e5926b22fc40c884bee1c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f000000000000000000000000000000000000000000000000000000000000000070eab9bae1c23390018084af5f","signature":["943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"],"raw_data":{"contract":[{"parameter":{"type_url":"type.googleapis.com/protocol.TriggerSmartContract","value":{"contract_address":"41a614f803b6fd780986a42c78ec9c7f77e6ded13c","data":"095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000","owner_address":"4160e00625a95cbc180f290e2611c826f90eeba56f"}},"type":"TriggerSmartContract"}],"expiration":1770267837000,"fee_limit":200000000,"ref_block_bytes":"af5b","ref_block_hash":"64a0e8e5926b22fc","timestamp":1770267778282},"txID":"fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205","visible":false},"signature":"943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00","result":true} From 5ce5f9b02ad3e489a78739d1dc1332ba67f49f5c Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Feb 2026 16:50:06 +0900 Subject: [PATCH 4/8] code cleanup --- gemstone/src/wallet_connect/response_handler/mod.rs | 2 +- gemstone/src/wallet_connect/response_handler/tron.rs | 8 ++------ .../test/tron_sign_transaction_response.json | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/gemstone/src/wallet_connect/response_handler/mod.rs b/gemstone/src/wallet_connect/response_handler/mod.rs index b3c556efd..c2dec9bf6 100644 --- a/gemstone/src/wallet_connect/response_handler/mod.rs +++ b/gemstone/src/wallet_connect/response_handler/mod.rs @@ -145,7 +145,7 @@ mod tests { let WalletConnectResponseType::Object { json: result_json } = result else { panic!("Expected Object response for Tron") }; - assert_eq!(result_json, r#"{"signature":["sig"],"result":true}"#); + assert_eq!(result_json, r#"{"signature":["sig"]}"#); } #[test] diff --git a/gemstone/src/wallet_connect/response_handler/tron.rs b/gemstone/src/wallet_connect/response_handler/tron.rs index c1582d64a..1d3ecf23c 100644 --- a/gemstone/src/wallet_connect/response_handler/tron.rs +++ b/gemstone/src/wallet_connect/response_handler/tron.rs @@ -13,12 +13,8 @@ impl ChainResponseHandler for TronResponseHandler { } } - fn encode_sign_transaction(transaction_id: String) -> WalletConnectResponseType { - let mut value = - serde_json::from_str::(&transaction_id).unwrap_or_else(|_| serde_json::Value::String(transaction_id)); - if let serde_json::Value::Object(map) = &mut value { - map.entry("result".to_string()).or_insert(serde_json::Value::Bool(true)); - } + fn encode_sign_transaction(transaction: String) -> WalletConnectResponseType { + let value = serde_json::from_str::(&transaction).unwrap_or_else(|_| serde_json::Value::String(transaction)); let json = match value { serde_json::Value::String(value) => value, _ => value.to_string(), diff --git a/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json index 6c38547da..9ab61813f 100644 --- a/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json +++ b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json @@ -1 +1 @@ -{"address":"TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM","transaction":{"raw_data_hex":"0a02af5b220864a0e8e5926b22fc40c884bee1c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f000000000000000000000000000000000000000000000000000000000000000070eab9bae1c23390018084af5f","signature":["943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"],"raw_data":{"contract":[{"parameter":{"type_url":"type.googleapis.com/protocol.TriggerSmartContract","value":{"contract_address":"41a614f803b6fd780986a42c78ec9c7f77e6ded13c","data":"095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000","owner_address":"4160e00625a95cbc180f290e2611c826f90eeba56f"}},"type":"TriggerSmartContract"}],"expiration":1770267837000,"fee_limit":200000000,"ref_block_bytes":"af5b","ref_block_hash":"64a0e8e5926b22fc","timestamp":1770267778282},"txID":"fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205","visible":false},"signature":"943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00","result":true} +{"address":"TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM","transaction":{"raw_data_hex":"0a02af5b220864a0e8e5926b22fc40c884bee1c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f000000000000000000000000000000000000000000000000000000000000000070eab9bae1c23390018084af5f","signature":["943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"],"raw_data":{"contract":[{"parameter":{"type_url":"type.googleapis.com/protocol.TriggerSmartContract","value":{"contract_address":"41a614f803b6fd780986a42c78ec9c7f77e6ded13c","data":"095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000","owner_address":"4160e00625a95cbc180f290e2611c826f90eeba56f"}},"type":"TriggerSmartContract"}],"expiration":1770267837000,"fee_limit":200000000,"ref_block_bytes":"af5b","ref_block_hash":"64a0e8e5926b22fc","timestamp":1770267778282},"txID":"fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205","visible":false},"signature":"943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"} From bcd561c10100ed7c3f1093ca5a677180b0fbd0c1 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:21:28 +0900 Subject: [PATCH 5/8] code cleanup --- Cargo.lock | 2 +- crates/gem_hash/Cargo.toml | 1 + crates/gem_hash/src/lib.rs | 1 + crates/gem_hash/src/sha2.rs | 11 +++++++ crates/gem_tron/Cargo.toml | 1 - crates/gem_tron/src/signer/chain_signer.rs | 4 +-- gemstone/src/models/transaction.rs | 4 +-- gemstone/src/wallet_connect/mod.rs | 7 +---- .../wallet_connect/response_handler/mod.rs | 15 ++++------ .../wallet_connect/response_handler/tron.rs | 29 ------------------- 10 files changed, 25 insertions(+), 50 deletions(-) create mode 100644 crates/gem_hash/src/sha2.rs delete mode 100644 gemstone/src/wallet_connect/response_handler/tron.rs diff --git a/Cargo.lock b/Cargo.lock index 1d3460e92..6eb5961a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3337,6 +3337,7 @@ dependencies = [ name = "gem_hash" version = "1.0.0" dependencies = [ + "sha2", "sha3", "tiny-keccak", ] @@ -3571,7 +3572,6 @@ dependencies = [ "serde", "serde_json", "settings", - "sha2", "signer", "tokio", ] diff --git a/crates/gem_hash/Cargo.toml b/crates/gem_hash/Cargo.toml index 0ea5a4eaa..734ed10c1 100644 --- a/crates/gem_hash/Cargo.toml +++ b/crates/gem_hash/Cargo.toml @@ -4,5 +4,6 @@ version = { workspace = true } edition = { workspace = true } [dependencies] +sha2 = { workspace = true } sha3 = { version = "0.10.8" } tiny-keccak = { workspace = true } diff --git a/crates/gem_hash/src/lib.rs b/crates/gem_hash/src/lib.rs index 969419f9b..d03a7da55 100644 --- a/crates/gem_hash/src/lib.rs +++ b/crates/gem_hash/src/lib.rs @@ -1,2 +1,3 @@ pub mod keccak; +pub mod sha2; pub mod sha3; diff --git a/crates/gem_hash/src/sha2.rs b/crates/gem_hash/src/sha2.rs new file mode 100644 index 000000000..232c0dab3 --- /dev/null +++ b/crates/gem_hash/src/sha2.rs @@ -0,0 +1,11 @@ +use sha2::{Digest, Sha256}; + +pub fn sha256(bytes: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(bytes); + let result = hasher.finalize(); + + let mut hash = [0u8; 32]; + hash.copy_from_slice(&result); + hash +} diff --git a/crates/gem_tron/Cargo.toml b/crates/gem_tron/Cargo.toml index 36b532668..a86b1bf14 100644 --- a/crates/gem_tron/Cargo.toml +++ b/crates/gem_tron/Cargo.toml @@ -7,7 +7,6 @@ edition = { workspace = true } bs58 = { workspace = true } hex = { workspace = true } primitives = { path = "../primitives" } -sha2 = { workspace = true } signer = { path = "../signer" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/gem_tron/src/signer/chain_signer.rs b/crates/gem_tron/src/signer/chain_signer.rs index a040fa5ac..65ad2df9d 100644 --- a/crates/gem_tron/src/signer/chain_signer.rs +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -10,8 +10,8 @@ use primitives::{ }; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; -use sha2::{Digest, Sha256}; use signer::{SignatureScheme, Signer}; +use gem_hash::sha2::sha256; #[derive(Debug, Clone, Serialize, Deserialize)] struct TronTransaction { @@ -43,7 +43,7 @@ fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result, } -pub type GemHyperliquidOrder = HyperliquidOrder; - #[uniffi::remote(Record)] pub struct GemHyperliquidOrder { pub approve_agent_required: bool, diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index 9eeb13fed..f7ef75bbe 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -494,12 +494,7 @@ impl WalletConnect { WalletConnectTransactionType::Bitcoin { output_type } => Ok(WalletConnectTransaction::Bitcoin { data, output_type }), WalletConnectTransactionType::Tron { output_type } => { let json: serde_json::Value = serde_json::from_str(&data)?; - if json.get("transaction").is_none() { - return Err(GemstoneError::AnyError { - msg: "Missing transaction field".to_string(), - }); - } - + let _ = json; Ok(WalletConnectTransaction::Tron { data, output_type }) } } diff --git a/gemstone/src/wallet_connect/response_handler/mod.rs b/gemstone/src/wallet_connect/response_handler/mod.rs index c2dec9bf6..c1166b53a 100644 --- a/gemstone/src/wallet_connect/response_handler/mod.rs +++ b/gemstone/src/wallet_connect/response_handler/mod.rs @@ -1,10 +1,6 @@ use crate::wallet_connect::handler_traits::ChainResponseHandler; use primitives::ChainType; -mod tron; - -use tron::TronResponseHandler; - #[derive(Debug, Clone, uniffi::Enum)] pub enum WalletConnectResponseType { String { value: String }, @@ -30,8 +26,7 @@ impl ChainResponseHandler for WalletConnectResponseHandler { impl WalletConnectResponseHandler { pub fn encode_sign_message(chain_type: ChainType, signature: String) -> WalletConnectResponseType { match chain_type { - ChainType::Tron => TronResponseHandler::encode_sign_message(signature), - ChainType::Solana | ChainType::Sui => { + ChainType::Solana | ChainType::Sui | ChainType::Tron => { let result = serde_json::json!({ "signature": signature }); @@ -46,7 +41,6 @@ impl WalletConnectResponseHandler { pub fn encode_sign_transaction(chain_type: ChainType, transaction_id: String) -> WalletConnectResponseType { match chain_type { - ChainType::Tron => TronResponseHandler::encode_sign_transaction(transaction_id), ChainType::Solana | ChainType::Ton => WalletConnectResponseType::Object { json: serde_json::json!({ "signature": transaction_id }).to_string(), }, @@ -65,16 +59,19 @@ impl WalletConnectResponseHandler { }; WalletConnectResponseType::Object { json: result.to_string() } } + ChainType::Tron => WalletConnectResponseType::Object { json: transaction_id }, _ => WalletConnectResponseType::String { value: transaction_id }, } } pub fn encode_send_transaction(chain_type: ChainType, transaction_id: String) -> WalletConnectResponseType { match chain_type { - ChainType::Tron => TronResponseHandler::encode_send_transaction(transaction_id), ChainType::Sui => WalletConnectResponseType::Object { json: serde_json::json!({ "digest": transaction_id }).to_string(), }, + ChainType::Tron => WalletConnectResponseType::Object { + json: serde_json::json!({ "result": true, "txid": transaction_id }).to_string(), + }, _ => WalletConnectResponseType::String { value: transaction_id }, } } @@ -145,7 +142,7 @@ mod tests { let WalletConnectResponseType::Object { json: result_json } = result else { panic!("Expected Object response for Tron") }; - assert_eq!(result_json, r#"{"signature":["sig"]}"#); + assert_eq!(result_json, json); } #[test] diff --git a/gemstone/src/wallet_connect/response_handler/tron.rs b/gemstone/src/wallet_connect/response_handler/tron.rs deleted file mode 100644 index 1d3ecf23c..000000000 --- a/gemstone/src/wallet_connect/response_handler/tron.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::wallet_connect::handler_traits::ChainResponseHandler; -use crate::wallet_connect::response_handler::WalletConnectResponseType; - -pub struct TronResponseHandler; - -impl ChainResponseHandler for TronResponseHandler { - fn encode_sign_message(signature: String) -> WalletConnectResponseType { - let result = serde_json::json!({ - "signature": signature - }); - WalletConnectResponseType::Object { - json: serde_json::to_string(&result).unwrap_or_default(), - } - } - - fn encode_sign_transaction(transaction: String) -> WalletConnectResponseType { - let value = serde_json::from_str::(&transaction).unwrap_or_else(|_| serde_json::Value::String(transaction)); - let json = match value { - serde_json::Value::String(value) => value, - _ => value.to_string(), - }; - WalletConnectResponseType::Object { json } - } - - fn encode_send_transaction(transaction_id: String) -> WalletConnectResponseType { - let json = serde_json::json!({ "result": true, "txid": transaction_id }).to_string(); - WalletConnectResponseType::Object { json } - } -} From f214f5a5ead62c52e83b338ef99e57d125e63331 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:40:47 +0900 Subject: [PATCH 6/8] remove raw_data_hex --- crates/gem_tron/src/provider/preload.rs | 1 - crates/gem_tron/src/signer/chain_signer.rs | 30 ++++--------------- .../src/transaction_load_metadata.rs | 1 - gemstone/src/models/transaction.rs | 5 ---- gemstone/src/wallet_connect/mod.rs | 1 - 5 files changed, 6 insertions(+), 32 deletions(-) diff --git a/crates/gem_tron/src/provider/preload.rs b/crates/gem_tron/src/provider/preload.rs index 07e7abf08..35b2a9288 100644 --- a/crates/gem_tron/src/provider/preload.rs +++ b/crates/gem_tron/src/provider/preload.rs @@ -44,7 +44,6 @@ impl ChainTransactionLoad for TronClient { parent_hash: block.parent_hash.clone(), witness_address: block.witness_address.clone(), stake_data, - raw_data_hex: None, }; let fee = match &input.input_type { diff --git a/crates/gem_tron/src/signer/chain_signer.rs b/crates/gem_tron/src/signer/chain_signer.rs index 65ad2df9d..d6605a190 100644 --- a/crates/gem_tron/src/signer/chain_signer.rs +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -1,13 +1,4 @@ -use primitives::{ - ChainSigner, - SignerError, - TransactionInputType, - TransactionLoadInput, - TransactionLoadMetadata, - TransferDataOutputAction, - TransferDataOutputType, - hex::decode_hex, -}; +use primitives::{ChainSigner, SignerError, TransactionInputType, TransactionLoadInput, TransferDataOutputAction, TransferDataOutputType, hex::decode_hex}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use signer::{SignatureScheme, Signer}; @@ -26,7 +17,6 @@ struct PayloadMetadata { payload: Value, output_type: TransferDataOutputType, output_action: TransferDataOutputAction, - raw_data_hex: String, } pub struct TronChainSigner; @@ -42,7 +32,11 @@ fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result Result<(TronTransaction, }; let transaction_value = map.get("transaction").cloned().ok_or_else(|| invalid_input("Missing transaction field"))?; let transaction: TronTransaction = serde_json::from_value(transaction_value)?; - let raw_data_hex = match &input.metadata { - TransactionLoadMetadata::Tron { - raw_data_hex: Some(raw_data_hex), - .. - } => raw_data_hex.clone(), - _ => transaction - .raw_data_hex - .clone() - .ok_or_else(|| invalid_input("Missing raw_data_hex in Tron transaction payload"))?, - }; - let metadata = PayloadMetadata { payload, output_type: extra.output_type.clone(), output_action: extra.output_action.clone(), - raw_data_hex, }; Ok((transaction, metadata)) } diff --git a/crates/primitives/src/transaction_load_metadata.rs b/crates/primitives/src/transaction_load_metadata.rs index 9812d2be3..c65bdfde7 100644 --- a/crates/primitives/src/transaction_load_metadata.rs +++ b/crates/primitives/src/transaction_load_metadata.rs @@ -88,7 +88,6 @@ pub enum TransactionLoadMetadata { parent_hash: String, witness_address: String, stake_data: TronStakeData, - raw_data_hex: Option, }, Sui { message_bytes: String, diff --git a/gemstone/src/models/transaction.rs b/gemstone/src/models/transaction.rs index 6bf84b692..624aaedfc 100644 --- a/gemstone/src/models/transaction.rs +++ b/gemstone/src/models/transaction.rs @@ -444,7 +444,6 @@ pub enum GemTransactionLoadMetadata { parent_hash: String, witness_address: String, stake_data: GemTronStakeData, - raw_data_hex: Option, }, Sui { message_bytes: String, @@ -527,7 +526,6 @@ impl From for GemTransactionLoadMetadata { parent_hash, witness_address, stake_data, - raw_data_hex, } => GemTransactionLoadMetadata::Tron { block_number, block_version, @@ -536,7 +534,6 @@ impl From for GemTransactionLoadMetadata { parent_hash, witness_address, stake_data, - raw_data_hex, }, TransactionLoadMetadata::Sui { message_bytes } => GemTransactionLoadMetadata::Sui { message_bytes }, TransactionLoadMetadata::Hyperliquid { order } => GemTransactionLoadMetadata::Hyperliquid { order }, @@ -617,7 +614,6 @@ impl From for TransactionLoadMetadata { parent_hash, witness_address, stake_data, - raw_data_hex, } => TransactionLoadMetadata::Tron { block_number, block_version, @@ -626,7 +622,6 @@ impl From for TransactionLoadMetadata { parent_hash, witness_address, stake_data, - raw_data_hex, }, GemTransactionLoadMetadata::Sui { message_bytes } => TransactionLoadMetadata::Sui { message_bytes }, GemTransactionLoadMetadata::Hyperliquid { order } => TransactionLoadMetadata::Hyperliquid { order }, diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index f7ef75bbe..9b3912efe 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -236,7 +236,6 @@ mod tests { parent_hash: "".to_string(), witness_address: "".to_string(), stake_data: TronStakeData::Votes(vec![]), - raw_data_hex: None, }, }; From c4d05596e883121108d7c613e8a816422fa42075 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:55:57 +0900 Subject: [PATCH 7/8] Update mod.rs --- gemstone/src/wallet_connect/mod.rs | 42 +++++++++--------------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index 9b3912efe..3d6a15bf1 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -8,7 +8,11 @@ fn current_timestamp() -> i64 { SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0) } -use crate::{GemstoneError, message::sign_type::{SignDigestType, SignMessage}, siwe::SiweMessage}; +use crate::{ + GemstoneError, + message::sign_type::{SignDigestType, SignMessage}, + siwe::SiweMessage, +}; pub mod actions; pub mod handler_traits; @@ -40,8 +44,8 @@ mod tests { use crate::message::signer::MessageSigner; use gem_tron::TronChainSigner; use primitives::{ - Asset, ChainSigner, GasPriceType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransferDataExtra, TransferDataOutputAction, - TransferDataOutputType, TronStakeData, WalletConnectionSessionAppMetadata, + Asset, ChainSigner, GasPriceType, TransactionInputType, TransactionLoadInput, TransactionLoadMetadata, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, + TronStakeData, WalletConnectionSessionAppMetadata, }; fn make_request(method: &str, params: &str, chain_id: Option<&str>) -> WalletConnectRequest { @@ -180,19 +184,10 @@ mod tests { fn parse_tron_sign_transaction_and_sign() { let wallet_connect = WalletConnect::new(); let params = include_str!("./test/tron_sign_transaction.json"); - let request = make_request( - "tron_signTransaction", - &serde_json::to_string(¶ms.trim()).unwrap(), - Some("tron:0x2b6653dc"), - ); + let request = make_request("tron_signTransaction", &serde_json::to_string(¶ms.trim()).unwrap(), Some("tron:0x2b6653dc")); let action = WalletConnectRequestHandler::parse_request(request).unwrap(); - let WalletConnectAction::SignTransaction { - chain, - transaction_type, - data, - } = action - else { + let WalletConnectAction::SignTransaction { chain, transaction_type, data } = action else { panic!("Expected SignTransaction action"); }; @@ -267,19 +262,10 @@ mod tests { #[test] fn parse_tron_send_transaction() { let params = include_str!("./test/tron_send_transaction.json"); - let request = make_request( - "tron_sendTransaction", - &serde_json::to_string(¶ms.trim()).unwrap(), - Some("tron:0x2b6653dc"), - ); + let request = make_request("tron_sendTransaction", &serde_json::to_string(¶ms.trim()).unwrap(), Some("tron:0x2b6653dc")); let action = WalletConnectRequestHandler::parse_request(request).unwrap(); - let WalletConnectAction::SendTransaction { - chain, - transaction_type, - data, - } = action - else { + let WalletConnectAction::SendTransaction { chain, transaction_type, data } = action else { panic!("Expected SendTransaction action"); }; @@ -491,11 +477,7 @@ impl WalletConnect { Ok(WalletConnectTransaction::Ton { messages, output_type }) } WalletConnectTransactionType::Bitcoin { output_type } => Ok(WalletConnectTransaction::Bitcoin { data, output_type }), - WalletConnectTransactionType::Tron { output_type } => { - let json: serde_json::Value = serde_json::from_str(&data)?; - let _ = json; - Ok(WalletConnectTransaction::Tron { data, output_type }) - } + WalletConnectTransactionType::Tron { output_type } => Ok(WalletConnectTransaction::Tron { data, output_type }), } } } From 44c596406d219866bc2386e14f0cac8c5fd5fda7 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Thu, 5 Feb 2026 21:48:25 +0900 Subject: [PATCH 8/8] Support Tron contract payloads & energy estimation --- crates/gem_tron/src/models/contract.rs | 62 +++++++++++++++++++ crates/gem_tron/src/models/mod.rs | 49 +++++++++++++++ crates/gem_tron/src/provider/preload.rs | 43 ++++++++++++- crates/gem_tron/src/rpc/client.rs | 49 +++++++++------ crates/gem_tron/src/signer/chain_signer.rs | 44 ++++--------- crates/gem_tron/src/signer/mod.rs | 1 + crates/gem_tron/src/signer/transaction.rs | 52 ++++++++++++++++ .../test/tron_sign_transaction_response.json | 2 +- 8 files changed, 249 insertions(+), 53 deletions(-) create mode 100644 crates/gem_tron/src/signer/transaction.rs diff --git a/crates/gem_tron/src/models/contract.rs b/crates/gem_tron/src/models/contract.rs index 612d887ad..72964f327 100644 --- a/crates/gem_tron/src/models/contract.rs +++ b/crates/gem_tron/src/models/contract.rs @@ -1,4 +1,10 @@ use serde::{Deserialize, Serialize}; +use std::error::Error; + +use crate::address::TronAddress; +use crate::signer::transaction::TronPayload; + +const TRIGGER_SMART_CONTRACT: &str = "TriggerSmartContract"; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TronSmartContractCall { @@ -23,3 +29,59 @@ pub struct TronSmartContractResultMessage { pub result: bool, pub message: Option, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TriggerSmartContractData { + pub contract_address: String, + pub data: String, + pub owner_address: String, + pub fee_limit: Option, + pub call_value: Option, +} + +impl TriggerSmartContractData { + pub fn from_payload( + data: Option<&[u8]>, + sender_address: &str, + ) -> Result, Box> { + let Some(data) = data else { + return Ok(None); + }; + let Ok(payload) = serde_json::from_slice::(data) else { + return Ok(None); + }; + let Some(raw_data) = payload.transaction.raw_data.as_ref() else { + return Ok(None); + }; + let Some(contract) = raw_data.contract.first() else { + return Ok(None); + }; + if contract.contract_type != TRIGGER_SMART_CONTRACT { + return Ok(None); + } + + let value = &contract.parameter.value; + let Some(contract_address) = value.contract_address.as_deref().and_then(TronAddress::from_hex) else { + return Err("Invalid Tron contract address".into()); + }; + let Some(data) = value.data.as_deref() else { + return Ok(None); + }; + let owner_address = if payload.address.is_empty() { + match value.owner_address.as_deref() { + Some(address) => TronAddress::from_hex(address).ok_or("Invalid Tron owner address")?, + None => sender_address.to_string(), + } + } else { + payload.address + }; + + Ok(Some(Self { + contract_address, + data: data.to_string(), + owner_address, + fee_limit: raw_data.fee_limit, + call_value: value.call_value, + })) + } +} diff --git a/crates/gem_tron/src/models/mod.rs b/crates/gem_tron/src/models/mod.rs index e75d9dc56..d014d7533 100644 --- a/crates/gem_tron/src/models/mod.rs +++ b/crates/gem_tron/src/models/mod.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::{error::Error, fmt}; pub mod account; pub mod block; @@ -124,6 +125,18 @@ pub struct TriggerConstantContractRequest { pub visible: bool, } +#[derive(Serialize, Debug)] +pub struct TriggerConstantContractDataRequest { + pub owner_address: String, + pub contract_address: String, + pub data: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fee_limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub call_value: Option, + pub visible: bool, +} + #[derive(Deserialize, Debug)] pub struct TriggerConstantContractResponse { #[serde(default)] @@ -136,10 +149,46 @@ pub struct TriggerConstantContractResponse { #[derive(Deserialize, Debug)] pub struct TriggerContractResult { + pub result: Option, pub code: Option, pub message: Option, } +impl TriggerConstantContractResponse { + pub fn check_error(&self) -> Option { + let result = self.result.as_ref()?; + if result.result.unwrap_or(false) { + return None; + } + + let message = result.message.as_deref().map(|message_hex| { + hex::decode(message_hex) + .ok() + .and_then(|bytes| String::from_utf8(bytes).ok()) + .unwrap_or_else(|| message_hex.to_string()) + }); + + Some(TronRpcError { + code: result.code.clone(), + message, + }) + } +} + +#[derive(Debug, Clone)] +pub struct TronRpcError { + pub code: Option, + pub message: Option, +} + +impl fmt::Display for TronRpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Tron RPC Error {} {}", self.code.as_deref().unwrap_or(""), self.message.as_deref().unwrap_or("")) + } +} + +impl Error for TronRpcError {} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WitnessesList { pub witnesses: Vec, diff --git a/crates/gem_tron/src/provider/preload.rs b/crates/gem_tron/src/provider/preload.rs index 35b2a9288..eea3f3090 100644 --- a/crates/gem_tron/src/provider/preload.rs +++ b/crates/gem_tron/src/provider/preload.rs @@ -9,10 +9,11 @@ use gem_client::Client; use number_formatter::BigNumberFormatter; use primitives::{ AssetSubtype, FeePriority, FeeRate, GasPriceType, StakeType, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, - TransactionPreloadInput, TronStakeData, TronVote, + TransactionPreloadInput, TransferDataOutputAction, TronStakeData, TronVote, }; use crate::{ + models::{ChainParameter, TriggerSmartContractData, account::TronAccountUsage}, provider::{ balances_mapper::format_address_parameter, preload_mapper::{calculate_stake_fee_rate, calculate_transfer_fee_rate, calculate_transfer_token_fee_rate, calculate_unfreeze_amounts}, @@ -61,6 +62,16 @@ impl ChainTransactionLoad for TronClient { .await? } }, + TransactionInputType::Generic(_, _, extra) => match extra.output_action { + TransferDataOutputAction::Send => match self + .estimate_fee_with_data(&input.sender_address, extra.data.as_deref(), &chain_parameters, &account_usage) + .await? + { + Some(fee) => fee, + None => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account)?), + }, + TransferDataOutputAction::Sign => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account)?), + }, TransactionInputType::Stake(_asset, stake_type) => TransactionFee::new_from_fee(calculate_stake_fee_rate(&chain_parameters, &account_usage, stake_type)?), TransactionInputType::Swap(from_asset, _, swap_data) => match &from_asset.id.token_id { None => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account)?), @@ -110,6 +121,36 @@ impl TronClient { )) } + async fn estimate_fee_with_data( + &self, + sender_address: &str, + data: Option<&[u8]>, + chain_parameters: &[ChainParameter], + account_usage: &TronAccountUsage, + ) -> Result, Box> { + let Some(parsed) = TriggerSmartContractData::from_payload(data, sender_address)? else { + return Ok(None); + }; + + let estimated_energy = self + .estimate_energy_with_data( + &parsed.owner_address, + &parsed.contract_address, + &parsed.data, + parsed.fee_limit, + parsed.call_value, + ) + .await?; + let token_fee = calculate_transfer_token_fee_rate(chain_parameters, account_usage, estimated_energy)?; + + Ok(Some(TransactionFee::new_gas_price_type( + GasPriceType::regular(BigInt::from(token_fee.energy_price)), + BigInt::from(token_fee.fee), + BigInt::from(token_fee.fee_limit), + HashMap::new(), + ))) + } + async fn get_is_new_account_for_input_type(&self, address: &str, input_type: TransactionInputType) -> Result> { match input_type { TransactionInputType::Transfer(asset) diff --git a/crates/gem_tron/src/rpc/client.rs b/crates/gem_tron/src/rpc/client.rs index 99e79ca6a..0d7ae2230 100644 --- a/crates/gem_tron/src/rpc/client.rs +++ b/crates/gem_tron/src/rpc/client.rs @@ -6,8 +6,8 @@ use std::{error::Error, str::FromStr}; use crate::address::TronAddress; use crate::models::{ - Block, BlockTransactions, BlockTransactionsInfo, ChainParameter, ChainParametersResponse, Transaction, TransactionReceiptData, TriggerConstantContractRequest, - TriggerConstantContractResponse, TronTransactionBroadcast, WitnessesList, + Block, BlockTransactions, BlockTransactionsInfo, ChainParameter, ChainParametersResponse, Transaction, TransactionReceiptData, TriggerConstantContractDataRequest, + TriggerConstantContractRequest, TriggerConstantContractResponse, TronTransactionBroadcast, WitnessesList, }; use crate::models::{TronAccount, TronAccountRequest, TronAccountUsage, TronBlock, TronEmptyAccount, TronReward, TronSmartContractCall, TronSmartContractResult}; use crate::rpc::constants::{DECIMALS_SELECTOR, DEFAULT_OWNER_ADDRESS, NAME_SELECTOR, SYMBOL_SELECTOR}; @@ -16,7 +16,6 @@ use alloy_primitives::Address as AlloyAddress; use alloy_sol_types::SolCall; use gem_client::Client; use gem_evm::contracts::erc20::{decode_abi_string, decode_abi_uint8}; -use serde_json::Value; #[derive(Clone)] pub struct TronClient { @@ -127,24 +126,38 @@ impl TronClient { visible: true, }; - let response: Value = self.client.post("/wallet/triggerconstantcontract", &request_payload, None).await?; - - if let Some(result_obj) = response.get("result") { - let is_success = result_obj.get("result").and_then(|value| value.as_bool()).unwrap_or(false); - if !is_success { - let code = result_obj.get("code").and_then(|v| v.as_str()).unwrap_or_default(); - let message_hex = result_obj.get("message").and_then(|v| v.as_str()).unwrap_or_default(); - let message = hex::decode(message_hex) - .ok() - .and_then(|bytes| String::from_utf8(bytes).ok()) - .unwrap_or_else(|| message_hex.to_string()); - return Err(format!("Estimate energy failed. Code: {}, Message: {}", code, message).into()); - } + let response = self.trigger_constant_contract_request(&request_payload).await?; + if let Some(error) = response.check_error() { + return Err(Box::new(error)); } + let energy_used = response.energy_used; + let energy_penalty = response.energy_penalty.unwrap_or_default(); + Ok(energy_used + energy_penalty) + } - let energy_used = response.get("energy_used").and_then(|value| value.as_u64()).unwrap_or_default(); - let energy_penalty = response.get("energy_penalty").and_then(|value| value.as_u64()).unwrap_or_default(); + pub async fn estimate_energy_with_data( + &self, + owner_address: &str, + contract_address: &str, + data: &str, + fee_limit: Option, + call_value: Option, + ) -> Result> { + let request_payload = TriggerConstantContractDataRequest { + owner_address: owner_address.to_string(), + contract_address: contract_address.to_string(), + data: data.to_string(), + fee_limit, + call_value, + visible: true, + }; + let response: TriggerConstantContractResponse = self.client.post("/wallet/triggerconstantcontract", &request_payload, None).await?; + if let Some(error) = response.check_error() { + return Err(Box::new(error)); + } + let energy_used = response.energy_used; + let energy_penalty = response.energy_penalty.unwrap_or_default(); Ok(energy_used + energy_penalty) } } diff --git a/crates/gem_tron/src/signer/chain_signer.rs b/crates/gem_tron/src/signer/chain_signer.rs index d6605a190..c1a2fb3c0 100644 --- a/crates/gem_tron/src/signer/chain_signer.rs +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -1,17 +1,8 @@ use primitives::{ChainSigner, SignerError, TransactionInputType, TransactionLoadInput, TransferDataOutputAction, TransferDataOutputType, hex::decode_hex}; -use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; +use serde_json::Value; use signer::{SignatureScheme, Signer}; use gem_hash::sha2::sha256; - -#[derive(Debug, Clone, Serialize, Deserialize)] -struct TronTransaction { - raw_data_hex: Option, - #[serde(default)] - signature: Vec, - #[serde(flatten)] - other: Map, -} +use super::transaction::{TronPayload, TronTransaction}; struct PayloadMetadata { payload: Value, @@ -62,18 +53,11 @@ fn extract_transaction(input: &TransactionLoadInput) -> Result<(TronTransaction, }; let data = extra.data.as_ref().ok_or_else(|| invalid_input("Missing transaction data"))?; - let payload = match serde_json::from_slice::(data)? { - Value::String(raw_json) => serde_json::from_str::(&raw_json).unwrap_or(Value::String(raw_json)), - value => value, - }; - - let Value::Object(map) = &payload else { - return Err(invalid_input("Invalid Tron transaction payload")); - }; - let transaction_value = map.get("transaction").cloned().ok_or_else(|| invalid_input("Missing transaction field"))?; - let transaction: TronTransaction = serde_json::from_value(transaction_value)?; + let payload: TronPayload = serde_json::from_slice(data).map_err(|_| invalid_input("Invalid Tron transaction payload"))?; + let transaction = payload.transaction.clone(); + let payload_value = serde_json::to_value(&payload).map_err(|_| invalid_input("Invalid Tron transaction payload"))?; let metadata = PayloadMetadata { - payload, + payload: payload_value, output_type: extra.output_type.clone(), output_action: extra.output_action.clone(), }; @@ -93,19 +77,13 @@ fn extract_transaction_value(payload: &Value) -> Option { } fn apply_signature(payload: Value, signature_hex: &str) -> Result { - let Value::Object(mut map) = payload else { - return Err(invalid_input("Invalid Tron transaction payload")); - }; - let transaction_value = map.get("transaction").cloned().ok_or_else(|| invalid_input("Missing transaction field"))?; - let mut transaction: TronTransaction = serde_json::from_value(transaction_value)?; - if !transaction.signature.is_empty() { + let mut payload: TronPayload = serde_json::from_value(payload).map_err(|_| invalid_input("Invalid Tron transaction payload"))?; + if !payload.transaction.signature.is_empty() { return Err(invalid_input("Tron multisig not supported for WalletConnect signing")); } - transaction.signature = vec![signature_hex.to_string()]; - map.insert("transaction".to_string(), serde_json::to_value(transaction)?); - map.entry("signature".to_string()) - .or_insert(serde_json::Value::String(signature_hex.to_string())); - Ok(Value::Object(map)) + payload.transaction.signature = vec![signature_hex.to_string()]; + payload.signature = Some(signature_hex.to_string()); + Ok(serde_json::to_value(payload)?) } fn invalid_input(message: impl Into) -> SignerError { diff --git a/crates/gem_tron/src/signer/mod.rs b/crates/gem_tron/src/signer/mod.rs index 46980fafa..82b7ec8c3 100644 --- a/crates/gem_tron/src/signer/mod.rs +++ b/crates/gem_tron/src/signer/mod.rs @@ -1,5 +1,6 @@ mod chain_signer; mod message; +pub(crate) mod transaction; pub use chain_signer::TronChainSigner; pub use message::tron_hash_message; diff --git a/crates/gem_tron/src/signer/transaction.rs b/crates/gem_tron/src/signer/transaction.rs new file mode 100644 index 000000000..8ee36f710 --- /dev/null +++ b/crates/gem_tron/src/signer/transaction.rs @@ -0,0 +1,52 @@ +use serde::{Deserialize, Serialize}; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct TronPayload { + pub(crate) address: String, + pub(crate) transaction: TronTransaction, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) signature: Option, +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct TronTransaction { + pub(crate) raw_data: Option, + pub(crate) raw_data_hex: Option, + #[serde(default)] + pub(crate) signature: Vec, + #[serde(rename = "txID")] + pub(crate) tx_id: Option, + pub(crate) visible: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct TronRawData { + pub(crate) contract: Vec, + pub(crate) expiration: Option, + pub(crate) fee_limit: Option, + pub(crate) ref_block_bytes: Option, + pub(crate) ref_block_hash: Option, + pub(crate) timestamp: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct TronContract { + pub(crate) parameter: TronContractParameter, + #[serde(rename = "type")] + pub(crate) contract_type: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct TronContractParameter { + pub(crate) type_url: String, + pub(crate) value: TronContractValue, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct TronContractValue { + pub(crate) contract_address: Option, + pub(crate) data: Option, + pub(crate) owner_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) call_value: Option, +} diff --git a/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json index 9ab61813f..6affc6cc7 100644 --- a/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json +++ b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json @@ -1 +1 @@ -{"address":"TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM","transaction":{"raw_data_hex":"0a02af5b220864a0e8e5926b22fc40c884bee1c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f000000000000000000000000000000000000000000000000000000000000000070eab9bae1c23390018084af5f","signature":["943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"],"raw_data":{"contract":[{"parameter":{"type_url":"type.googleapis.com/protocol.TriggerSmartContract","value":{"contract_address":"41a614f803b6fd780986a42c78ec9c7f77e6ded13c","data":"095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000","owner_address":"4160e00625a95cbc180f290e2611c826f90eeba56f"}},"type":"TriggerSmartContract"}],"expiration":1770267837000,"fee_limit":200000000,"ref_block_bytes":"af5b","ref_block_hash":"64a0e8e5926b22fc","timestamp":1770267778282},"txID":"fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205","visible":false},"signature":"943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"} +{"address":"TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM","transaction":{"raw_data":{"contract":[{"parameter":{"type_url":"type.googleapis.com/protocol.TriggerSmartContract","value":{"contract_address":"41a614f803b6fd780986a42c78ec9c7f77e6ded13c","data":"095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000","owner_address":"4160e00625a95cbc180f290e2611c826f90eeba56f"}},"type":"TriggerSmartContract"}],"expiration":1770267837000,"fee_limit":200000000,"ref_block_bytes":"af5b","ref_block_hash":"64a0e8e5926b22fc","timestamp":1770267778282},"raw_data_hex":"0a02af5b220864a0e8e5926b22fc40c884bee1c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f000000000000000000000000000000000000000000000000000000000000000070eab9bae1c23390018084af5f","signature":["943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"],"txID":"fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205","visible":false},"signature":"943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00"}