diff --git a/Cargo.lock b/Cargo.lock index a67e5cafc..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", ] @@ -3561,6 +3562,7 @@ dependencies = [ "futures", "gem_client", "gem_evm", + "gem_hash", "hex", "num-bigint", "num-traits", @@ -3570,6 +3572,7 @@ dependencies = [ "serde", "serde_json", "settings", + "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 27845df25..a86b1bf14 100644 --- a/crates/gem_tron/Cargo.toml +++ b/crates/gem_tron/Cargo.toml @@ -7,6 +7,7 @@ edition = { workspace = true } bs58 = { workspace = true } hex = { workspace = true } primitives = { path = "../primitives" } +signer = { path = "../signer" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -18,6 +19,7 @@ number_formatter = { path = "../number_formatter", 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/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 new file mode 100644 index 000000000..c1a2fb3c0 --- /dev/null +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -0,0 +1,91 @@ +use primitives::{ChainSigner, SignerError, TransactionInputType, TransactionLoadInput, TransferDataOutputAction, TransferDataOutputType, hex::decode_hex}; +use serde_json::Value; +use signer::{SignatureScheme, Signer}; +use gem_hash::sha2::sha256; +use super::transaction::{TronPayload, TronTransaction}; + +struct PayloadMetadata { + payload: Value, + output_type: TransferDataOutputType, + output_action: TransferDataOutputAction, +} + +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(invalid_input("Tron multisig not supported for WalletConnect signing")); + } + let raw_data_hex = transaction + .raw_data_hex + .as_deref() + .ok_or_else(|| invalid_input("Missing raw_data_hex in Tron transaction payload"))?; + let raw_bytes = decode_hex(raw_data_hex)?; + let digest = sha256(&raw_bytes); + let signature = sign_digest(&digest, private_key)?; + 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_value(&payload) + .ok_or_else(|| invalid_input("Missing transaction object for Tron broadcast"))?, + 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(invalid_input("Expected generic transaction input")); + }; + let data = extra.data.as_ref().ok_or_else(|| invalid_input("Missing transaction data"))?; + + 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_value, + output_type: extra.output_type.clone(), + output_action: extra.output_action.clone(), + }; + 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_value(payload: &Value) -> Option { + match payload { + Value::Object(map) => map.get("transaction").cloned(), + _ => None, + } +} + +fn apply_signature(payload: Value, signature_hex: &str) -> Result { + 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")); + } + 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 { + SignerError::InvalidInput(message.into()) +} 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..82b7ec8c3 --- /dev/null +++ b/crates/gem_tron/src/signer/mod.rs @@ -0,0 +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/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/wallet_connect_namespace.rs b/crates/primitives/src/wallet_connect_namespace.rs index 0f2c1b4e0..c194bac2f 100644 --- a/crates/primitives/src/wallet_connect_namespace.rs +++ b/crates/primitives/src/wallet_connect_namespace.rs @@ -13,6 +13,8 @@ pub enum WalletConnectCAIP2 { Algorand, Sui, Ton, + #[serde(rename = "tron")] + Tron, Bip122, } @@ -25,8 +27,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, } } @@ -38,6 +41,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), } } @@ -58,6 +62,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), } } @@ -71,7 +76,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, } } @@ -102,6 +108,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); } @@ -113,6 +120,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) @@ -128,6 +136,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/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..3d6a15bf1 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -8,8 +8,11 @@ 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; @@ -38,6 +41,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 { [ @@ -126,6 +145,142 @@ 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![]), + }, + }; + + 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(); @@ -168,7 +323,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 +331,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 +350,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 +424,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 +436,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 +452,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 +469,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 +477,7 @@ impl WalletConnect { Ok(WalletConnectTransaction::Ton { messages, output_type }) } WalletConnectTransactionType::Bitcoin { output_type } => Ok(WalletConnectTransaction::Bitcoin { data, output_type }), + WalletConnectTransactionType::Tron { 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 947f78a9a..8e811608a 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; @@ -21,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; @@ -60,6 +66,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..ffcae329b --- /dev/null +++ b/gemstone/src/wallet_connect/request_handler/tron.rs @@ -0,0 +1,101 @@ +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; + +// https://docs.reown.com/advanced/multichain/rpc-reference/tron-rpc +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() { + 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() { + 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_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 { + 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()); + } +} diff --git a/gemstone/src/wallet_connect/response_handler.rs b/gemstone/src/wallet_connect/response_handler/mod.rs similarity index 81% rename from gemstone/src/wallet_connect/response_handler.rs rename to gemstone/src/wallet_connect/response_handler/mod.rs index 422689a86..c1166b53a 100644 --- a/gemstone/src/wallet_connect/response_handler.rs +++ b/gemstone/src/wallet_connect/response_handler/mod.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 }, } } @@ -68,6 +69,9 @@ impl WalletConnectResponseHandler { 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 }, } } @@ -110,6 +114,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 +135,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()); @@ -166,6 +192,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/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..6affc6cc7 --- /dev/null +++ b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json @@ -0,0 +1 @@ +{"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"}