diff --git a/crates/gem_tron/src/models/contract.rs b/crates/gem_tron/src/models/contract.rs index dd0394c38..4e49ba864 100644 --- a/crates/gem_tron/src/models/contract.rs +++ b/crates/gem_tron/src/models/contract.rs @@ -47,7 +47,12 @@ impl TriggerSmartContractData { let Ok(payload) = serde_json::from_slice::(data) else { return Ok(None); }; - let Some(raw_data_value) = payload.transaction.other.get("raw_data") else { + let raw_data_value = payload + .transaction + .other + .get("raw_data") + .or_else(|| payload.transaction.other.get("transaction").and_then(|t| t.get("raw_data"))); + let Some(raw_data_value) = raw_data_value else { return Ok(None); }; let Ok(raw_data) = serde_json::from_value::(raw_data_value.clone()) else { diff --git a/crates/gem_tron/src/signer/chain_signer.rs b/crates/gem_tron/src/signer/chain_signer.rs index a4d9e75f8..83d0fb566 100644 --- a/crates/gem_tron/src/signer/chain_signer.rs +++ b/crates/gem_tron/src/signer/chain_signer.rs @@ -1,69 +1,81 @@ -use super::transaction::{TronPayload, TronTransaction}; use gem_hash::sha2::sha256; -use primitives::{ChainSigner, SignerError, TransactionLoadInput, TransferDataOutputAction, TransferDataOutputType, hex::decode_hex}; +use primitives::{ChainSigner, SignerError, TransactionLoadInput, TransferDataOutputType, hex::decode_hex}; use serde_json::Value; use signer::{SignatureScheme, Signer}; -struct PayloadMetadata { +enum PayloadFormat { + V1, + Legacy, +} + +struct TronPayload { payload: Value, + format: PayloadFormat, output_type: TransferDataOutputType, - output_action: TransferDataOutputAction, } -pub struct TronChainSigner; +impl TronPayload { + fn parse(input: &TransactionLoadInput) -> Result { + let extra = input.get_data_extra().map_err(SignerError::invalid_input)?; + let data = extra.data.as_ref().ok_or_else(|| SignerError::invalid_input("Missing transaction data"))?; + let payload: Value = serde_json::from_slice(data)?; -impl ChainSigner for TronChainSigner { - fn sign_data(&self, input: &TransactionLoadInput, private_key: &[u8]) -> Result { - sign_data(input, private_key) - } -} + let transaction = payload + .get("transaction") + .ok_or_else(|| SignerError::invalid_input("Missing transaction in Tron payload"))?; -fn sign_data(input: &TransactionLoadInput, private_key: &[u8]) -> Result { - let (transaction, metadata) = get_transaction(input)?; - let raw_data_hex = transaction - .raw_data_hex - .as_deref() - .ok_or_else(|| SignerError::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 = Signer::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key.to_vec()).map_err(|e| SignerError::signing_error(e.to_string()))?; - let signature_hex = hex::encode(signature); + let format = if transaction.get("raw_data_hex").is_some() { + PayloadFormat::V1 + } else if transaction.get("transaction").and_then(|t| t.get("raw_data_hex")).is_some() { + PayloadFormat::Legacy + } else { + return Err(SignerError::invalid_input("Missing raw_data_hex in Tron transaction payload")); + }; - 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 => payload - .get("transaction") - .cloned() - .ok_or_else(|| SignerError::invalid_input("Missing transaction object for Tron broadcast"))?, - TransferDataOutputAction::Sign => payload, - }; + Ok(Self { + payload, + format, + output_type: extra.output_type.clone(), + }) + } - Ok(serde_json::to_string(&result_payload)?) + fn transaction(&self) -> &Value { + let transaction = &self.payload["transaction"]; + match self.format { + PayloadFormat::V1 => transaction, + PayloadFormat::Legacy => &transaction["transaction"], } } -} -fn get_transaction(input: &TransactionLoadInput) -> Result<(TronTransaction, PayloadMetadata), SignerError> { - let extra = input.get_data_extra().map_err(SignerError::invalid_input)?; - let data = extra.data.as_ref().ok_or_else(|| SignerError::invalid_input("Missing transaction data"))?; + fn raw_data_hex(&self) -> Result<&str, SignerError> { + self.transaction()["raw_data_hex"] + .as_str() + .ok_or_else(|| SignerError::invalid_input("raw_data_hex must be a string")) + } - let payload: TronPayload = serde_json::from_slice(data).map_err(|_| SignerError::invalid_input("Invalid Tron transaction payload"))?; - let transaction = payload.transaction.clone(); - let payload_value = serde_json::to_value(&payload).map_err(|_| SignerError::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 into_signed(self, signature_hex: &str) -> Result { + let mut transaction = self.transaction().clone(); + transaction + .as_object_mut() + .ok_or_else(|| SignerError::invalid_input("Transaction is not an object"))? + .insert("signature".to_string(), serde_json::json!([signature_hex])); + serde_json::to_string(&transaction).map_err(Into::into) + } } -fn apply_signature(payload: Value, signature_hex: &str) -> Result { - let mut payload: TronPayload = serde_json::from_value(payload).map_err(|_| SignerError::invalid_input("Invalid Tron transaction payload"))?; - payload.transaction.signature = Some(vec![signature_hex.to_string()]); - payload.signature = Some(signature_hex.to_string()); - Ok(serde_json::to_value(payload)?) +pub struct TronChainSigner; + +impl ChainSigner for TronChainSigner { + fn sign_data(&self, input: &TransactionLoadInput, private_key: &[u8]) -> Result { + let payload = TronPayload::parse(input)?; + let raw_bytes = decode_hex(payload.raw_data_hex()?)?; + let digest = sha256(&raw_bytes); + let signature = Signer::sign_digest(SignatureScheme::Secp256k1, digest.to_vec(), private_key.to_vec()).map_err(|e| SignerError::signing_error(e.to_string()))?; + let signature_hex = hex::encode(signature); + + match payload.output_type { + TransferDataOutputType::Signature => Ok(signature_hex), + TransferDataOutputType::EncodedTransaction => payload.into_signed(&signature_hex), + } + } } diff --git a/gemstone/justfile b/gemstone/justfile index 29ccc2177..d1a5eb70c 100644 --- a/gemstone/justfile +++ b/gemstone/justfile @@ -17,7 +17,7 @@ integration-test: build-integration-tests: cargo test --test integration_test --no-run --features reqwest_provider -export ANDROID_HOME := env_var_or_default("ANDROID_HOME", "~/Library/Android/sdk") +export ANDROID_HOME := env("ANDROID_HOME", "~/Library/Android/sdk") export HOST_ARCH := arch() install-ndk: @@ -54,7 +54,7 @@ export FW_NAME := "Gemstone" export STATIC_LIB_NAME := "lib" + LIB_NAME + ".a" export DY_LIB_NAME := if os() == "macos" { "libgemstone.dylib" } else { "libgemstone.so" } -export BUILD_MODE_ENV := env_var_or_default("BUILD_MODE", "debug") +export BUILD_MODE_ENV := env("BUILD_MODE", "debug") export BUILD_MODE_TARGET := if BUILD_MODE_ENV == "release" { "release" } else { "debug" } export BUILD_FLAG := if BUILD_MODE_TARGET == "release" { "--release" } else { "" } @@ -67,7 +67,7 @@ export TARGET_XC_FW_FOLDER := "target/spm" export FW_FFI_NAME := FW_NAME + "FFI" export FW_FFI_FILE := FW_FFI_NAME + ".framework" export XC_FW_FFI_NAME := FW_FFI_NAME + ".xcframework" -export DEPLOYMENT_TARGET := env_var_or_default("IPHONEOS_DEPLOYMENT_TARGET", "17.0") +export DEPLOYMENT_TARGET := env("IPHONEOS_DEPLOYMENT_TARGET", "17.0") export LIB_OUTPUT_PATH := TARGET_DIR + "/" + BUILD_MODE_TARGET + "/" + DY_LIB_NAME build-ios: build-targets bindgen-swift assemble-frameworks xcframework cp-xcframework-source diff --git a/gemstone/src/wallet_connect/mod.rs b/gemstone/src/wallet_connect/mod.rs index 92bc9fe3e..cc48b86a3 100644 --- a/gemstone/src/wallet_connect/mod.rs +++ b/gemstone/src/wallet_connect/mod.rs @@ -217,24 +217,53 @@ mod tests { let signature_payload = TronChainSigner.sign_data(&input, &TEST_PRIVATE_KEY).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, + value["signature"][0].as_str().unwrap(), "943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00" ); + assert!(value["txID"].as_str().is_some()); + assert!(value["raw_data_hex"].as_str().is_some()); + assert_eq!(value["visible"], serde_json::Value::Bool(false)); let response = wallet_connect.encode_sign_transaction(Chain::Tron, signature_payload.clone()); let expected: serde_json::Value = serde_json::from_str(include_str!("./test/tron_sign_transaction_response.json")).unwrap(); assert_eq!(response_json(&response), expected); } + #[test] + fn parse_tron_sign_transaction_nested_and_sign() { + let params = include_str!("./test/tron_sign_transaction_nested.json"); + let expected_data: serde_json::Value = serde_json::from_str(params.trim()).unwrap(); + let expected_data = expected_data.to_string(); + + let input = TransactionLoadInput { + input_type: TransactionInputType::Generic( + Asset::from_chain(Chain::Tron), + WalletConnectionSessionAppMetadata::mock(), + TransferDataExtra::mock_encoded_transaction(expected_data.as_bytes().to_vec()), + ), + 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::mock_tron(), + }; + + let signature_payload = TronChainSigner.sign_data(&input, &TEST_PRIVATE_KEY).unwrap(); + let value: serde_json::Value = serde_json::from_str(&signature_payload).unwrap(); + + assert_eq!( + value["signature"][0].as_str().unwrap(), + "943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00" + ); + assert!(value["txID"].as_str().is_some()); + assert!(value["raw_data_hex"].as_str().is_some()); + assert_eq!(value["visible"], serde_json::Value::Bool(false)); + } + #[test] fn parse_tron_send_transaction() { let params = include_str!("./test/tron_send_transaction.json"); diff --git a/gemstone/src/wallet_connect/test/tron_sign_transaction_nested.json b/gemstone/src/wallet_connect/test/tron_sign_transaction_nested.json new file mode 100644 index 000000000..64c9ae42c --- /dev/null +++ b/gemstone/src/wallet_connect/test/tron_sign_transaction_nested.json @@ -0,0 +1,30 @@ +{ + "address": "TJoSEwEqt7cT3TUwmEoUYnYs5cZR3xSukM", + "transaction": { + "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 index c962cb3d2..8c96fe591 100644 --- a/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json +++ b/gemstone/src/wallet_connect/test/tron_sign_transaction_response.json @@ -1,32 +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", - "signature": [ - "943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00" + "raw_data": { + "contract": [ + { + "parameter": { + "type_url": "type.googleapis.com/protocol.TriggerSmartContract", + "value": { + "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", + "data": "095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f0000000000000000000000000000000000000000000000000000000000000000", + "owner_address": "4160e00625a95cbc180f290e2611c826f90eeba56f" + } + }, + "type": "TriggerSmartContract" + } ], - "txID": "fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205", - "visible": false + "expiration": 1770267837000, + "fee_limit": 200000000, + "ref_block_bytes": "af5b", + "ref_block_hash": "64a0e8e5926b22fc", + "timestamp": 1770267778282 }, - "signature": "943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00" + "raw_data_hex": "0a02af5b220864a0e8e5926b22fc40c884bee1c2335aae01081f12a9010a31747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e54726967676572536d617274436f6e747261637412740a154160e00625a95cbc180f290e2611c826f90eeba56f121541a614f803b6fd780986a42c78ec9c7f77e6ded13c2244095ea7b300000000000000000000000060e00625a95cbc180f290e2611c826f90eeba56f000000000000000000000000000000000000000000000000000000000000000070eab9bae1c23390018084af5f", + "signature": [ + "943d286dfd1fb6a2cd31c9af7a6cfd23ee062ec2e0abcf82c7daa0c7bb43ab04458e0e88ebe3a94060122cccc8fb4395e5eb922720327df04ae840139c729a1f00" + ], + "txID": "fb21f360363fcf23f80bbf33f28c1d9972f0bf5e13f20e48430000c88ce88205", + "visible": false }