From dee32a4515e255d962baed3358a4d93cd2b8936e Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 1 Jun 2026 13:27:18 +0900 Subject: [PATCH 1/2] Add Chainflip TRON vault swap support --- core/Cargo.lock | 1 + core/crates/gem_tron/src/address/mod.rs | 33 ++++- .../gem_tron/src/models/signing/raw_data.rs | 4 +- core/crates/gem_tron/src/provider/preload.rs | 39 +++++- .../gem_tron/src/signer/chain_signer.rs | 120 +++++++++++------- .../crates/gem_tron/src/signer/transaction.rs | 97 +++++++++++--- .../primitives/src/testkit/swap_mock.rs | 31 ++++- .../testkit/transaction_load_input_mock.rs | 24 ++++ core/crates/swapper/Cargo.toml | 1 + .../swapper/src/chainflip/broker/client.rs | 1 + .../swapper/src/chainflip/broker/model.rs | 56 +++++++- .../swapper/src/chainflip/client/model.rs | 3 + core/crates/swapper/src/chainflip/provider.rs | 62 ++++++++- 13 files changed, 392 insertions(+), 80 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 79ab60c28e..027e1c2481 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -7680,6 +7680,7 @@ dependencies = [ "gem_solana", "gem_sui", "gem_ton", + "gem_tron", "hex", "hmac 0.13.0", "num-bigint", diff --git a/core/crates/gem_tron/src/address/mod.rs b/core/crates/gem_tron/src/address/mod.rs index 2cbf359b47..6afe8963bc 100644 --- a/core/crates/gem_tron/src/address/mod.rs +++ b/core/crates/gem_tron/src/address/mod.rs @@ -25,14 +25,18 @@ pub struct TronAddress([u8; PREFIXED_ADDRESS_LEN]); impl TronAddress { pub fn from_hex(hex_value: &str) -> Option { let bytes = decode_hex(hex_value).ok()?; - if bytes.len() != PREFIXED_ADDRESS_LEN || bytes.first() != Some(&ADDRESS_PREFIX) { - return None; + match bytes.as_slice() { + [ADDRESS_PREFIX, payload @ ..] if payload.len() == ADDRESS_LEN => Some(Self(bytes.try_into().ok()?)), + payload if payload.len() == ADDRESS_LEN => { + let account_id: [u8; ADDRESS_LEN] = payload.try_into().ok()?; + Some(Self::from(account_id)) + } + _ => None, } - Some(Self(bytes.try_into().ok()?)) } - #[cfg(feature = "signer")] - pub(crate) fn from_hex_or_base58(value: &str) -> Option { + + pub fn from_hex_or_base58(value: &str) -> Option { // v3-compatible raw transaction parsing prefers base58 when both formats are technically valid. Self::try_parse(value).or_else(|| Self::from_hex(value)) } @@ -123,6 +127,10 @@ mod tests { TronAddress::from_hex("41357a7401a0f0c2d4a44a1881a0c622f15d986291").unwrap().to_string(), "TEqyWRKCzREYC2bK2fc3j7pp8XjAa6tJK1" ); + assert_eq!( + TronAddress::from_hex("357a7401a0f0c2d4a44a1881a0c622f15d986291").unwrap().to_string(), + "TEqyWRKCzREYC2bK2fc3j7pp8XjAa6tJK1" + ); assert_eq!(TronAddress::from_hex("42357a7401a0f0c2d4a44a1881a0c622f15d986291"), None); } @@ -155,13 +163,26 @@ mod tests { assert_eq!(TronAddress::try_parse(&unprefixed).unwrap().as_bytes(), expected); } - #[cfg(feature = "signer")] #[test] fn test_from_hex_or_base58() { let expected = hex::decode("41357a7401a0f0c2d4a44a1881a0c622f15d986291").unwrap(); + let chainflip_expected = hex::decode("412523ae929fecd9d665f472f59b99a8ce6b179510").unwrap(); assert_eq!(TronAddress::from_hex_or_base58("TEqyWRKCzREYC2bK2fc3j7pp8XjAa6tJK1").unwrap().as_bytes(), expected); assert_eq!(TronAddress::from_hex_or_base58("41357a7401a0f0c2d4a44a1881a0c622f15d986291").unwrap().as_bytes(), expected); + assert_eq!(TronAddress::from_hex_or_base58("357a7401a0f0c2d4a44a1881a0c622f15d986291").unwrap().as_bytes(), expected); + assert_eq!( + TronAddress::from_hex_or_base58("TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi").unwrap().as_bytes(), + chainflip_expected + ); + assert_eq!( + TronAddress::from_hex_or_base58("412523ae929fecd9d665f472f59b99a8ce6b179510").unwrap().as_bytes(), + chainflip_expected + ); + assert_eq!( + TronAddress::from_hex_or_base58("2523ae929fecd9d665f472f59b99a8ce6b179510").unwrap().as_bytes(), + chainflip_expected + ); assert_eq!(TronAddress::from_hex_or_base58("invalid"), None); } diff --git a/core/crates/gem_tron/src/models/signing/raw_data.rs b/core/crates/gem_tron/src/models/signing/raw_data.rs index df65d59b1a..8fd2b9e536 100644 --- a/core/crates/gem_tron/src/models/signing/raw_data.rs +++ b/core/crates/gem_tron/src/models/signing/raw_data.rs @@ -24,7 +24,7 @@ pub(crate) struct TronRawData { } impl TronRawData { - pub(crate) fn from_input(input: &SignerInput, contract: TronContract, fee_limit: u64) -> Result { + pub(crate) fn from_input_with_data(input: &SignerInput, contract: TronContract, fee_limit: u64, data: Option>) -> Result { let TransactionLoadMetadata::Tron { block_number, block_version, @@ -62,7 +62,7 @@ impl TronRawData { expiration: block_timestamp .checked_add(EXPIRATION_DURATION_MS) .ok_or_else(|| SignerError::invalid_input("Tron expiration overflow"))?, - data: input.get_memo().map(|memo| memo.as_bytes().to_vec()), + data: data.or_else(|| input.get_memo().map(|memo| memo.as_bytes().to_vec())), contract, timestamp: *block_timestamp, fee_limit, diff --git a/core/crates/gem_tron/src/provider/preload.rs b/core/crates/gem_tron/src/provider/preload.rs index b9c1ba11f1..185e84d80e 100644 --- a/core/crates/gem_tron/src/provider/preload.rs +++ b/core/crates/gem_tron/src/provider/preload.rs @@ -9,7 +9,8 @@ use gem_client::Client; use number_formatter::BigNumberFormatter; use primitives::{ AssetSubtype, FeePriority, FeeRate, GasPriceType, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata, - TransactionPreloadInput, TransferDataOutputAction, TronStakeData, + TransactionPreloadInput, TransferDataOutputAction, TronStakeData, decode_hex, + swap::{SwapQuoteData, SwapQuoteDataType}, }; use crate::{ @@ -72,9 +73,13 @@ impl ChainTransactionLoad for TronClient { TransferDataOutputAction::Sign => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, false)?), }, 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, has_memo)?), - Some(token_id) => { + TransactionInputType::Swap(from_asset, _, swap_data) => match (&from_asset.id.token_id, &swap_data.data.data_type) { + (_, SwapQuoteDataType::Contract) if !swap_data.data.data.is_empty() => { + self.estimate_swap_contract_fee(&input.sender_address, &swap_data.data, &chain_parameters, &account_usage) + .await? + } + (None, _) => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_memo)?), + (Some(token_id), SwapQuoteDataType::Transfer) => { self.estimate_token_transfer_fee( input.sender_address.clone(), swap_data.data.to.clone(), @@ -85,6 +90,7 @@ impl ChainTransactionLoad for TronClient { ) .await? } + (Some(_), SwapQuoteDataType::Contract) => return Err("Tron token contract swap calldata is required".into()), }, _ => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_memo)?), }; @@ -119,6 +125,31 @@ impl TronClient { )) } + async fn estimate_swap_contract_fee( + &self, + sender_address: &str, + data: &SwapQuoteData, + chain_parameters: &[ChainParameter], + account_usage: &TronAccountUsage, + ) -> Result> { + let contract_data = TriggerSmartContractData { + contract_address: data.to.clone(), + data: hex::encode(decode_hex(&data.data)?), + owner_address: sender_address.to_string(), + fee_limit: None, + call_value: Some(data.value.parse::()?).filter(|value| *value > 0), + }; + let estimated_energy = self.estimate_energy_with_data(&contract_data).await?; + let token_fee = calculate_transfer_token_fee_rate(chain_parameters, account_usage, estimated_energy)?; + + Ok(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 estimate_fee_with_data( &self, sender_address: &str, diff --git a/core/crates/gem_tron/src/signer/chain_signer.rs b/core/crates/gem_tron/src/signer/chain_signer.rs index f3cc1fc8cc..ba98be648d 100644 --- a/core/crates/gem_tron/src/signer/chain_signer.rs +++ b/core/crates/gem_tron/src/signer/chain_signer.rs @@ -41,9 +41,10 @@ mod tests { use gem_hash::sha2::sha256; use num_bigint::BigInt; use primitives::{ - Asset, AssetId, AssetType, Chain, ChainSigner, Delegation, DelegationValidator, GasPriceType, Resource, SignerInput, StakeType, TransactionFee, TransactionInputType, - TransactionLoadInput, TransactionLoadMetadata, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, TronStakeData, TronUnfreeze, TronVote, + Asset, AssetId, AssetType, Chain, ChainSigner, Delegation, DelegationValidator, GasPriceType, Resource, SignerInput, StakeType, SwapProvider, TransactionFee, + TransactionInputType, TransactionLoadMetadata, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, TronStakeData, TronUnfreeze, TronVote, WalletConnectionSessionAppMetadata, decode_hex, + swap::{SwapData, SwapQuote, SwapQuoteData}, }; use serde_json::{Value, json}; @@ -91,32 +92,8 @@ mod tests { TransactionFee::new_gas_price_type(GasPriceType::regular(0), BigInt::from(fee), BigInt::from(gas_limit), HashMap::new()) } - fn signer_input( - input_type: TransactionInputType, - sender: &str, - destination: &str, - value: &str, - transaction_fee: TransactionFee, - memo: Option<&str>, - metadata: TransactionLoadMetadata, - ) -> SignerInput { - SignerInput::new( - TransactionLoadInput { - input_type, - sender_address: sender.to_string(), - destination_address: destination.to_string(), - value: value.to_string(), - gas_price: GasPriceType::regular(0), - memo: memo.map(str::to_string), - is_max_value: false, - metadata, - }, - transaction_fee, - ) - } - fn native_input(value: &str, transaction_fee: TransactionFee, memo: Option<&str>) -> SignerInput { - signer_input( + SignerInput::mock_tron( TransactionInputType::Transfer(Asset::from_chain(Chain::Tron)), SENDER, RECIPIENT, @@ -183,7 +160,7 @@ mod tests { // https://github.com/trustwallet/wallet-core/blob/master/tests/chains/Tron/SignerTests.cpp #[test] fn sign_transfer_with_memo_matches_wallet_core() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Transfer(Asset::from_chain(Chain::Tron)), "TFnYQCt892UNjn67pjAULTSTkB7YvqsnPp", "TBUCzgc29vykkvFaEG2mgRtxKvaKe6skwX", @@ -213,7 +190,7 @@ mod tests { #[test] fn sign_token_transfer_builds_trc20_trigger_contract() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Transfer(trc20_asset(RECIPIENT)), SENDER, "TW1dU4L3eNm7Lw8WvieLKEHpXWAussRG9Z", @@ -239,7 +216,7 @@ mod tests { #[test] fn sign_token_transfer_uses_gas_limit_as_fee_limit() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Transfer(trc20_asset(RECIPIENT)), SENDER, "TW1dU4L3eNm7Lw8WvieLKEHpXWAussRG9Z", @@ -254,10 +231,10 @@ mod tests { } fn swap_input(use_max_amount: Option, min_from_value: Option<&str>, value: &str, transaction_fee: TransactionFee) -> SignerInput { - let mut swap_data = primitives::swap::SwapData::mock_transfer(primitives::SwapProvider::Okx, value, "1", "TW1dU4L3eNm7Lw8WvieLKEHpXWAussRG9Z"); + let mut swap_data = SwapData::mock_transfer(SwapProvider::Okx, value, "1", "TW1dU4L3eNm7Lw8WvieLKEHpXWAussRG9Z"); swap_data.quote.use_max_amount = use_max_amount; swap_data.quote.min_from_value = min_from_value.map(str::to_string); - signer_input( + SignerInput::mock_tron( TransactionInputType::Swap(Asset::from_chain(Chain::Tron), Asset::from_chain(Chain::Tron), swap_data), SENDER, RECIPIENT, @@ -305,11 +282,68 @@ mod tests { assert!(TronChainSigner.sign_swap(&input, &private_key()).is_err()); } + #[test] + fn sign_contract_swap_native_transfer_attaches_hex_memo() { + let note = "0x03001111111111111111111111111111111111111111008101010a0000002523ae929fecd9d665f472f59b99a8ce6b1795100000000000000000000000000000000000000000000000000000000000000000000000009e8d88ae895c9b37b2dead9757a3452f7c2299704d91ddfa444d87723f94fe0c000000"; + let input = SignerInput::mock_tron( + TransactionInputType::Swap( + Asset::from_chain(Chain::Tron), + Asset::from_chain(Chain::Ethereum), + SwapData::mock_with_quote_data( + SwapQuote::mock_with_addresses(SwapProvider::Okx, NILE_SENDER, "10000000", "0x1111111111111111111111111111111111111111", "1"), + SwapQuoteData::mock_contract_call("TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi", "10000000", "", Some(note)), + ), + ), + NILE_SENDER, + "TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi", + "10000000", + TransactionFee::default(), + Some(note), + nile_metadata(TronStakeData::Votes(vec![])), + ); + + let output = signed_json(TronChainSigner.sign_swap(&input, &nile_private_key()).unwrap().remove(0)); + + assert_eq!(output["raw_data"]["contract"][0]["type"], "TransferContract"); + assert_eq!(output["raw_data"]["data"], note.trim_start_matches("0x")); + } + + #[test] + fn sign_contract_swap_token_uses_quote_calldata() { + let note = "0x03001111111111111111111111111111111111111111008901010a0000002523ae929fecd9d665f472f59b99a8ce6b17951000000000000000000000000000000000000000000000000000000000000000000001320000009e8d88ae895c9b37b2dead9757a3452f7c2299704d91ddfa444d87723f94fe0c000000"; + let calldata = "0xa9059cbb0000000000000000000000002523ae929fecd9d665f472f59b99a8ce6b17951000000000000000000000000000000000000000000000000000000000004c4b40"; + let source_token_address = "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"; + let input = SignerInput::mock_tron( + TransactionInputType::Swap( + trc20_asset(source_token_address), + Asset::from_chain(Chain::Ethereum), + SwapData::mock_with_quote_data( + SwapQuote::mock_with_addresses(SwapProvider::Okx, NILE_SENDER, "5000000", "0x1111111111111111111111111111111111111111", "1"), + SwapQuoteData::mock_contract_call(source_token_address, "0", calldata, Some(note)), + ), + ), + NILE_SENDER, + "TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi", + "5000000", + fee(0, 100_000_000), + Some(note), + nile_metadata(TronStakeData::Votes(vec![])), + ); + + let output = signed_json(TronChainSigner.sign_swap(&input, &nile_private_key()).unwrap().remove(0)); + let value = &output["raw_data"]["contract"][0]["parameter"]["value"]; + + assert_eq!(output["raw_data"]["contract"][0]["type"], "TriggerSmartContract"); + assert_eq!(value["contract_address"], "41eca9bc828a3005b9a3b909f2cc5c2a54794de05f"); + assert_eq!(value["data"], calldata.trim_start_matches("0x")); + assert_eq!(output["raw_data"]["data"], note.trim_start_matches("0x")); + } + // Source vector: // https://github.com/trustwallet/wallet-core/blob/master/tests/chains/Tron/SignerTests.cpp #[test] fn sign_vote_witness_matches_wallet_core() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Stake(validator(RECIPIENT))), SENDER, RECIPIENT, @@ -332,7 +366,7 @@ mod tests { #[test] fn sign_vote_witness_keeps_multiple_votes() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Stake(validator(RECIPIENT))), SENDER, RECIPIENT, @@ -362,7 +396,7 @@ mod tests { #[test] fn sign_unstake_votes_builds_vote_witness_contract() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Unstake(Delegation::mock_tron(RECIPIENT))), SENDER, RECIPIENT, @@ -388,7 +422,7 @@ mod tests { #[test] fn sign_freeze_v2_builds_energy_contract() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Freeze(Resource::Energy)), NILE_SENDER, RECIPIENT, @@ -408,7 +442,7 @@ mod tests { #[test] fn sign_unfreeze_v2_builds_contract() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Unfreeze(Resource::Energy)), NILE_SENDER, RECIPIENT, @@ -429,7 +463,7 @@ mod tests { // https://github.com/trustwallet/wallet-core/blob/master/tests/chains/Tron/SignerTests.cpp #[test] fn sign_withdraw_rewards_matches_wallet_core() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Rewards(vec![])), SENDER, RECIPIENT, @@ -449,7 +483,7 @@ mod tests { #[test] fn sign_withdraw_expire_unfreeze_builds_contract() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Withdraw(Delegation::mock_tron(RECIPIENT))), NILE_SENDER, RECIPIENT, @@ -470,7 +504,7 @@ mod tests { #[test] fn sign_unstake_unfreeze_outputs_one_transaction_per_unfreeze() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Stake(Asset::from_chain(Chain::Tron), StakeType::Unstake(Delegation::mock_tron(RECIPIENT))), SENDER, RECIPIENT, @@ -525,7 +559,7 @@ mod tests { fn generic_payload(payload: Value, output_type: TransferDataOutputType) -> SignerInput { let payload = serde_json::to_vec(&payload).unwrap(); - signer_input( + SignerInput::mock_tron( TransactionInputType::Generic( Asset::from_chain(Chain::Tron), WalletConnectionSessionAppMetadata { @@ -679,7 +713,7 @@ mod tests { #[test] fn sign_transfer_rejects_invalid_address() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Transfer(Asset::from_chain(Chain::Tron)), SENDER, "INVALID_NOT_BASE58", @@ -707,7 +741,7 @@ mod tests { #[test] fn sign_transfer_rejects_invalid_metadata() { - let input = signer_input( + let input = SignerInput::mock_tron( TransactionInputType::Transfer(Asset::from_chain(Chain::Tron)), SENDER, RECIPIENT, diff --git a/core/crates/gem_tron/src/signer/transaction.rs b/core/crates/gem_tron/src/signer/transaction.rs index bf7784c8b3..be08812b63 100644 --- a/core/crates/gem_tron/src/signer/transaction.rs +++ b/core/crates/gem_tron/src/signer/transaction.rs @@ -1,6 +1,9 @@ use gem_hash::sha2::sha256; use num_bigint::BigUint; -use primitives::{Address as _, SignerError, SignerInput, StakeType, TransactionLoadMetadata, TronStakeData}; +use primitives::{ + Address as _, SignerError, SignerInput, StakeType, TransactionLoadMetadata, TronStakeData, decode_hex, + swap::{SwapQuoteData, SwapQuoteDataType}, +}; use signer::{SignatureScheme, Signer}; use crate::address::TronAddress; @@ -9,40 +12,93 @@ use crate::models::{SignedTransactionJson, TronContract, TronRawData, TronResour const ABI_WORD_LEN: usize = 32; const TRC20_TRANSFER_SELECTOR: [u8; 4] = [0xa9, 0x05, 0x9c, 0xbb]; +struct ContractPayload { + contract: TronContract, + fee_limit: u64, + data: Option>, +} + pub(crate) fn sign_transfer(input: &SignerInput, private_key: &[u8]) -> Result { - sign_native_transfer(input, &input.destination_address, input.value_as_u64()?, private_key) + let owner = validate_sender(input, private_key)?; + let payload = build_native_transfer(input, owner, &input.destination_address, input.value_as_u64()?)?; + sign_contract_payload(input, payload, private_key) } pub(crate) fn sign_token_transfer(input: &SignerInput, private_key: &[u8]) -> Result { - sign_token_transfer_to(input, &input.destination_address, private_key) + let owner = validate_sender(input, private_key)?; + let payload = build_token_transfer(input, owner, &input.destination_address)?; + sign_contract_payload(input, payload, private_key) } pub(crate) fn sign_swap(input: &SignerInput, private_key: &[u8]) -> Result, SignerError> { let swap = input.input_type.get_swap_data()?; + let swap_data = &swap.data; let from_asset = input.input_type.get_asset(); + let owner = validate_sender(input, private_key)?; - let result = if from_asset.id.is_token() { - sign_token_transfer_to(input, &swap.data.to, private_key)? - } else { - sign_native_transfer(input, &swap.data.to, input.swap_value_u64()?, private_key)? + let payload = match swap_data.data_type { + SwapQuoteDataType::Transfer => { + if from_asset.id.is_token() { + build_token_transfer(input, owner, &swap_data.to)? + } else { + build_native_transfer(input, owner, &swap_data.to, input.swap_value_u64()?)? + } + } + SwapQuoteDataType::Contract => build_contract_swap(input, owner, swap_data)?, }; - Ok(vec![result]) + Ok(vec![sign_contract_payload(input, payload, private_key)?]) } -fn sign_native_transfer(input: &SignerInput, destination: &str, value: u64, private_key: &[u8]) -> Result { - let owner = validate_sender(input, private_key)?; +fn build_native_transfer(input: &SignerInput, owner: TronAddress, destination: &str, value: u64) -> Result { let contract = TronContract::Transfer { owner, to: TronAddress::parse(destination)?, amount: value, }; let fee_limit = input.fee.fee()?; - sign_contract(input, contract, fee_limit, private_key) + Ok(ContractPayload { contract, fee_limit, data: None }) } -fn sign_token_transfer_to(input: &SignerInput, destination: &str, private_key: &[u8]) -> Result { - let owner = validate_sender(input, private_key)?; +fn build_contract_swap(input: &SignerInput, owner: TronAddress, swap_data: &SwapQuoteData) -> Result { + let from_asset = input.input_type.get_asset(); + let call_data = if swap_data.data.is_empty() { Vec::new() } else { decode_hex(&swap_data.data)? }; + let memo = swap_data.memo.as_deref().map(decode_hex).transpose()?; + + if from_asset.id.is_token() || !call_data.is_empty() { + build_contract_call_swap(input, owner, swap_data, call_data, memo) + } else { + build_native_contract_transfer_swap(input, owner, swap_data, memo) + } +} + +fn build_native_contract_transfer_swap(input: &SignerInput, owner: TronAddress, swap_data: &SwapQuoteData, memo: Option>) -> Result { + let to = TronAddress::from_hex_or_base58(&swap_data.to).ok_or_else(|| SignerError::invalid_input(format!("invalid Tron address: {}", swap_data.to)))?; + let amount = swap_data.value.parse::().map_err(|_| SignerError::invalid_input("invalid Tron swap value"))?; + let contract = TronContract::Transfer { owner, to, amount }; + let fee_limit = input.fee.fee()?; + Ok(ContractPayload { contract, fee_limit, data: memo }) +} + +fn build_contract_call_swap(input: &SignerInput, owner: TronAddress, swap_data: &SwapQuoteData, call_data: Vec, memo: Option>) -> Result { + if call_data.is_empty() { + return SignerError::invalid_input_err("Tron contract swap calldata is required"); + } + let contract_address = TronAddress::from_hex_or_base58(&swap_data.to).ok_or_else(|| SignerError::invalid_input(format!("invalid Tron address: {}", swap_data.to)))?; + let call_value = swap_data.value.parse::().map_err(|_| SignerError::invalid_input("invalid Tron contract call value"))?; + let contract = TronContract::TriggerSmart { + owner, + contract: contract_address, + data: call_data, + call_value: (call_value != 0).then_some(call_value), + call_token_value: None, + token_id: None, + }; + let fee_limit = input.fee.gas_limit()?; + Ok(ContractPayload { contract, fee_limit, data: memo }) +} + +fn build_token_transfer(input: &SignerInput, owner: TronAddress, destination: &str) -> Result { let token_id = input.input_type.get_asset().id.get_token_id()?; let destination = TronAddress::parse(destination)?; let contract = TronContract::TriggerSmart { @@ -54,7 +110,7 @@ fn sign_token_transfer_to(input: &SignerInput, destination: &str, private_key: & token_id: None, }; let fee_limit = input.fee.gas_limit()?; - sign_contract(input, contract, fee_limit, private_key) + Ok(ContractPayload { contract, fee_limit, data: None }) } pub(crate) fn sign_stake(input: &SignerInput, private_key: &[u8]) -> Result, SignerError> { @@ -95,7 +151,10 @@ pub(crate) fn sign_stake(input: &SignerInput, private_key: &[u8]) -> Result Result { @@ -114,8 +173,12 @@ fn validate_sender(input: &SignerInput, private_key: &[u8]) -> Result Result { - let raw_data = TronRawData::from_input(input, contract, fee_limit)?; +fn sign_contract_payload(input: &SignerInput, payload: ContractPayload, private_key: &[u8]) -> Result { + let raw_data = TronRawData::from_input_with_data(input, payload.contract, payload.fee_limit, payload.data)?; + sign_raw_data(raw_data, private_key) +} + +fn sign_raw_data(raw_data: TronRawData, private_key: &[u8]) -> Result { let raw_data_bytes = raw_data.encode(); let transaction_id = sha256(&raw_data_bytes); let signature = sign_raw_hash(&transaction_id, private_key)?; diff --git a/core/crates/primitives/src/testkit/swap_mock.rs b/core/crates/primitives/src/testkit/swap_mock.rs index d93b9efc63..cba6750c11 100644 --- a/core/crates/primitives/src/testkit/swap_mock.rs +++ b/core/crates/primitives/src/testkit/swap_mock.rs @@ -73,6 +73,10 @@ impl SwapData { } } + pub fn mock_with_quote_data(quote: SwapQuote, data: SwapQuoteData) -> Self { + SwapData { quote, data } + } + pub fn mock_contract(provider: SwapProvider, from_value: &str, to_value: &str, value: &str) -> Self { let swap_data = Self::mock_with_values(provider, from_value, to_value); SwapData { @@ -91,6 +95,13 @@ impl SwapData { ..swap_data } } + + pub fn mock_transfer_with_addresses(provider: SwapProvider, from_address: &str, from_value: &str, to_address: &str, to_value: &str, to: &str) -> Self { + SwapData { + quote: SwapQuote::mock_with_addresses(provider, from_address, from_value, to_address, to_value), + data: SwapQuoteData::new_tranfer(to.to_string(), from_value.to_string(), None), + } + } } impl SwapQuote { @@ -113,13 +124,17 @@ impl SwapQuote { } pub fn mock_with_values(provider: SwapProvider, from_value: &str, to_value: &str) -> Self { + Self::mock_with_addresses(provider, TEST_EVM_RECIPIENT, from_value, TEST_EVM_RECIPIENT, to_value) + } + + pub fn mock_with_addresses(provider: SwapProvider, from_address: &str, from_value: &str, to_address: &str, to_value: &str) -> Self { SwapQuote { from_value: from_value.to_string(), min_from_value: None, to_value: to_value.to_string(), provider_data: SwapProviderData::mock_with_provider(provider), - from_address: TEST_EVM_RECIPIENT.to_string(), - to_address: TEST_EVM_RECIPIENT.to_string(), + from_address: from_address.to_string(), + to_address: to_address.to_string(), slippage_bps: 50, eta_in_seconds: Some(30), use_max_amount: None, @@ -143,6 +158,18 @@ impl SwapQuoteData { pub fn mock_with_gas_limit(gas_limit: Option) -> Self { SwapQuoteData { gas_limit, ..Self::mock() } } + + pub fn mock_contract_call(to: &str, value: &str, data: &str, memo: Option<&str>) -> Self { + SwapQuoteData { + data_type: SwapQuoteDataType::Contract, + to: to.to_string(), + value: value.to_string(), + data: data.to_string(), + memo: memo.map(String::from), + approval: None, + gas_limit: None, + } + } } impl SwapProviderData { diff --git a/core/crates/primitives/src/testkit/transaction_load_input_mock.rs b/core/crates/primitives/src/testkit/transaction_load_input_mock.rs index 20cb09c066..30aea4aa28 100644 --- a/core/crates/primitives/src/testkit/transaction_load_input_mock.rs +++ b/core/crates/primitives/src/testkit/transaction_load_input_mock.rs @@ -99,6 +99,30 @@ impl SignerInput { ) } + pub fn mock_tron( + input_type: TransactionInputType, + sender: &str, + destination: &str, + value: &str, + transaction_fee: TransactionFee, + memo: Option<&str>, + metadata: TransactionLoadMetadata, + ) -> Self { + SignerInput::new( + TransactionLoadInput { + input_type, + sender_address: sender.to_string(), + destination_address: destination.to_string(), + value: value.to_string(), + gas_price: GasPriceType::regular(0), + memo: memo.map(str::to_string), + is_max_value: false, + metadata, + }, + transaction_fee, + ) + } + pub fn mock_osmosis(input_type: TransactionInputType, destination: &str) -> Self { let fee_amount = BigInt::from(10_000u64); SignerInput::new( diff --git a/core/crates/swapper/Cargo.toml b/core/crates/swapper/Cargo.toml index 6c100a87d6..bfd120f7be 100644 --- a/core/crates/swapper/Cargo.toml +++ b/core/crates/swapper/Cargo.toml @@ -18,6 +18,7 @@ gem_evm = { path = "../gem_evm", features = ["rpc"] } gem_sui = { path = "../gem_sui", features = ["rpc"] } gem_aptos = { path = "../gem_aptos", features = ["rpc"] } gem_cosmos = { path = "../gem_cosmos" } +gem_tron = { path = "../gem_tron", default-features = false } gem_hash = { path = "../gem_hash" } gem_jsonrpc = { path = "../gem_jsonrpc", features = ["client"] } gem_client = { path = "../gem_client" } diff --git a/core/crates/swapper/src/chainflip/broker/client.rs b/core/crates/swapper/src/chainflip/broker/client.rs index 1caa4bec50..c83db912fd 100644 --- a/core/crates/swapper/src/chainflip/broker/client.rs +++ b/core/crates/swapper/src/chainflip/broker/client.rs @@ -36,6 +36,7 @@ where ) -> Result { let extra_params_json = match extra_params { VaultSwapExtras::Evm(evm) => serde_json::to_value(evm).unwrap(), + VaultSwapExtras::Tron(tron) => serde_json::to_value(tron).unwrap(), VaultSwapExtras::Solana(sol) => serde_json::to_value(sol).unwrap(), VaultSwapExtras::None => Value::Null, }; diff --git a/core/crates/swapper/src/chainflip/broker/model.rs b/core/crates/swapper/src/chainflip/broker/model.rs index 621261a156..12a6c1a690 100644 --- a/core/crates/swapper/src/chainflip/broker/model.rs +++ b/core/crates/swapper/src/chainflip/broker/model.rs @@ -23,13 +23,14 @@ pub struct DcaParameters { #[derive(Debug)] pub enum VaultSwapExtras { - Evm(VaultSwapEvmExtras), + Evm(VaultSwapChainExtras), + Tron(VaultSwapChainExtras), Solana(VaultSwapSolanaExtras), None, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VaultSwapEvmExtras { +pub struct VaultSwapChainExtras { pub chain: String, #[serde(deserialize_with = "deserialize_biguint_from_hex_str", serialize_with = "serialize_biguint_to_hex_str")] pub input_amount: BigUint, @@ -48,6 +49,7 @@ pub struct VaultSwapSolanaExtras { #[derive(Debug, Clone, Deserialize)] #[serde(untagged)] pub enum VaultSwapResponse { + Tron(TronVaultSwapResponse), Evm(EvmVaultSwapResponse), Solana(SolanaVaultSwapResponse), } @@ -73,3 +75,53 @@ pub struct AccountMeta { pub is_writable: bool, pub pubkey: String, } + +#[derive(Debug, Clone, Deserialize)] +pub struct TronVaultSwapResponse { + pub calldata: String, + #[serde(deserialize_with = "deserialize_biguint_from_hex_str")] + pub value: BigUint, + pub to: String, + pub note: String, + pub source_token_address: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tron_vault_swap_response_deserializes_before_evm() { + let response: VaultSwapResponse = serde_json::from_value(serde_json::json!({ + "calldata": "0xa9059cbb", + "value": "0x0", + "to": "0x2523ae929fecd9d665f472f59b99a8ce6b179510", + "note": "0x0300", + "source_token_address": "0xeca9bc828a3005b9a3b909f2cc5c2a54794de05f" + })) + .unwrap(); + + let VaultSwapResponse::Tron(response) = response else { + panic!("expected Tron vault swap response"); + }; + assert_eq!(response.value, BigUint::from(0u32)); + assert_eq!(response.note, "0x0300"); + assert_eq!(response.source_token_address, Some("0xeca9bc828a3005b9a3b909f2cc5c2a54794de05f".to_string())); + } + + #[test] + fn test_evm_vault_swap_response_keeps_original_shape() { + let response: VaultSwapResponse = serde_json::from_value(serde_json::json!({ + "calldata": "0x1234", + "value": "0x3e8", + "to": "0x1111111111111111111111111111111111111111" + })) + .unwrap(); + + let VaultSwapResponse::Evm(response) = response else { + panic!("expected EVM vault swap response"); + }; + assert_eq!(response.value, BigUint::from(1000u32)); + assert_eq!(response.to, "0x1111111111111111111111111111111111111111"); + } +} diff --git a/core/crates/swapper/src/chainflip/client/model.rs b/core/crates/swapper/src/chainflip/client/model.rs index e5537985f3..7c0bafa9e8 100644 --- a/core/crates/swapper/src/chainflip/client/model.rs +++ b/core/crates/swapper/src/chainflip/client/model.rs @@ -111,6 +111,7 @@ fn chainflip_chain_to_chain(chain: &str) -> Option { "Bitcoin" => Some(Chain::Bitcoin), "Solana" => Some(Chain::Solana), "Arbitrum" => Some(Chain::Arbitrum), + "Tron" => Some(Chain::Tron), _ => None, } } @@ -126,6 +127,8 @@ static CHAINFLIP_ASSETS: LazyLock> = LazyLoc (Chain::Solana, "SOL", AssetId::from_chain(Chain::Solana)), (Chain::Solana, "USDC", SOLANA_USDC.id.clone()), (Chain::Solana, "USDT", SOLANA_USDT.id.clone()), + (Chain::Tron, "TRX", AssetId::from_chain(Chain::Tron)), + (Chain::Tron, "USDT", TRON_USDT.id.clone()), (Chain::Arbitrum, "ETH", AssetId::from_chain(Chain::Arbitrum)), (Chain::Arbitrum, "USDC", ARBITRUM_USDC.id.clone()), (Chain::Arbitrum, "USDT", ARBITRUM_USDT.id.clone()), diff --git a/core/crates/swapper/src/chainflip/provider.rs b/core/crates/swapper/src/chainflip/provider.rs index 18610f0e66..962785b022 100644 --- a/core/crates/swapper/src/chainflip/provider.rs +++ b/core/crates/swapper/src/chainflip/provider.rs @@ -1,13 +1,16 @@ use alloy_primitives::{U256, hex}; use async_trait::async_trait; use gem_client::Client; +use gem_tron::address::TronAddress; use num_bigint::BigUint; use num_traits::ToPrimitive; use std::{fmt::Debug, sync::Arc}; use super::{ ChainflipRouteData, - broker::{BrokerClient, ChainflipAsset, DcaParameters, RefundParameters, VaultSwapEvmExtras, VaultSwapExtras, VaultSwapResponse, VaultSwapSolanaExtras}, + broker::{ + BrokerClient, ChainflipAsset, DcaParameters, RefundParameters, TronVaultSwapResponse, VaultSwapChainExtras, VaultSwapExtras, VaultSwapResponse, VaultSwapSolanaExtras, + }, capitalize::capitalize_first_letter, client::{CHAINFLIP_SUPPORTED_ASSETS, ChainflipClient, QuoteRequest as ChainflipQuoteRequest, QuoteResponse, map_swap_result}, price::{apply_slippage, price_to_hex_price}, @@ -24,7 +27,12 @@ use crate::{ fees::DEFAULT_CHAINFLIP_FEE_BPS, solana::DEFAULT_SWAP_GAS_LIMIT, }; -use primitives::{Asset, ChainType, chain::Chain, swap::QuoteAsset}; +use primitives::{ + Asset, + ChainType, + chain::Chain, + swap::{QuoteAsset, SwapQuoteDataType::Contract}, +}; const DEFAULT_SWAP_ERC20_GAS_LIMIT: u64 = 100_000; @@ -77,7 +85,7 @@ fn map_asset_id(asset: &QuoteAsset) -> ChainflipAsset { fn build_quote_request(request: &QuoteRequest) -> Result { match request.from_asset.chain().chain_type() { - ChainType::Ethereum | ChainType::Solana => {} + ChainType::Ethereum | ChainType::Solana | ChainType::Tron => {} _ => return Err(SwapperError::NotSupportedChain), } let from_value = request.value.clone(); @@ -100,6 +108,20 @@ fn build_quote_request(request: &QuoteRequest) -> Result Option { + let address = response.source_token_address.as_deref().unwrap_or(&response.to); + + Some(SwapperQuoteData { + to: TronAddress::from_hex(address).map(|address| address.to_string())?, + data_type: Contract, + value: response.value.to_string(), + data: response.calldata.clone(), + memo: Some(response.note.clone()), + approval: None, + gas_limit: None, + }) +} + fn get_best_quote(mut quotes: Vec, fee_bps: u32) -> (BigUint, u32, u32, ChainflipRouteData) { quotes.sort_by(|a, b| b.egress_amount.cmp(&a.egress_amount)); let quote = "es[0]; @@ -231,7 +253,7 @@ where let base_asset_decimals = quote.request.from_asset.decimals; let min_price = price_to_hex_price(price_slippage, quote_asset_decimals, base_asset_decimals).map_err(SwapperError::TransactionError)?; let extra_params = match from_asset.chain.chain_type() { - ChainType::Ethereum => VaultSwapExtras::Evm(VaultSwapEvmExtras { + ChainType::Ethereum => VaultSwapExtras::Evm(VaultSwapChainExtras { chain, input_amount: input_amount.clone(), refund_parameters: RefundParameters { @@ -240,6 +262,15 @@ where min_price, }, }), + ChainType::Tron => VaultSwapExtras::Tron(VaultSwapChainExtras { + chain, + input_amount: input_amount.clone(), + refund_parameters: RefundParameters { + retry_duration: 10, + refund_address: quote.request.wallet_address.clone(), + min_price, + }, + }), ChainType::Solana => VaultSwapExtras::Solana(VaultSwapSolanaExtras { from: quote.request.wallet_address.clone(), seed: hex::encode_prefixed(generate_random_seed(32)), @@ -290,6 +321,10 @@ where Ok(SwapperQuoteData::new_contract(response.to, value, response.calldata, approval, gas_limit)) } + VaultSwapResponse::Tron(response) => { + let address = response.source_token_address.as_deref().unwrap_or(&response.to); + map_tron_quote_data(&response).ok_or_else(|| SwapperError::TransactionError(format!("invalid Tron address: {address}"))) + } VaultSwapResponse::Solana(response) => { let data = tx_builder::build_solana_tx("e.request.wallet_address, &response, self.rpc_provider.clone()) .await @@ -396,6 +431,25 @@ mod tests { ); } + #[test] + fn test_tron_quote_data_maps_contract_call_fields() { + let note = "0x0300"; + let data = map_tron_quote_data(&TronVaultSwapResponse { + calldata: "0xa9059cbb".to_string(), + value: BigUint::from(0u32), + to: "0x2523ae929fecd9d665f472f59b99a8ce6b179510".to_string(), + note: note.to_string(), + source_token_address: Some("0xeca9bc828a3005b9a3b909f2cc5c2a54794de05f".to_string()), + }) + .unwrap(); + + assert!(matches!(data.data_type, Contract)); + assert_eq!(data.to, "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"); + assert_eq!(data.memo, Some(note.to_string())); + assert_eq!(data.value, "0"); + assert_eq!(data.data, "0xa9059cbb"); + } + #[tokio::test] #[cfg(feature = "swap_integration_tests")] async fn test_get_swap_result() -> Result<(), Box> { From a8a9f36b723cbda3b21de86c49fb275289947a08 Mon Sep 17 00:00:00 2001 From: 0xh3rman <119309671+0xh3rman@users.noreply.github.com> Date: Mon, 1 Jun 2026 20:18:23 +0900 Subject: [PATCH 2/2] fix: handle Chainflip TRON quote data response --- core/crates/gem_tron/src/address/mod.rs | 1 - core/crates/gem_tron/src/provider/preload.rs | 60 +++++++++---- .../primitives/src/testkit/swap_mock.rs | 7 -- core/crates/swapper/src/chainflip/provider.rs | 84 ++++++++++++++----- 4 files changed, 108 insertions(+), 44 deletions(-) diff --git a/core/crates/gem_tron/src/address/mod.rs b/core/crates/gem_tron/src/address/mod.rs index 6afe8963bc..2c4e23582d 100644 --- a/core/crates/gem_tron/src/address/mod.rs +++ b/core/crates/gem_tron/src/address/mod.rs @@ -35,7 +35,6 @@ impl TronAddress { } } - pub fn from_hex_or_base58(value: &str) -> Option { // v3-compatible raw transaction parsing prefers base58 when both formats are technically valid. Self::try_parse(value).or_else(|| Self::from_hex(value)) diff --git a/core/crates/gem_tron/src/provider/preload.rs b/core/crates/gem_tron/src/provider/preload.rs index 185e84d80e..582213e464 100644 --- a/core/crates/gem_tron/src/provider/preload.rs +++ b/core/crates/gem_tron/src/provider/preload.rs @@ -73,25 +73,28 @@ impl ChainTransactionLoad for TronClient { TransferDataOutputAction::Sign => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, false)?), }, 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, &swap_data.data.data_type) { - (_, SwapQuoteDataType::Contract) if !swap_data.data.data.is_empty() => { - self.estimate_swap_contract_fee(&input.sender_address, &swap_data.data, &chain_parameters, &account_usage) + TransactionInputType::Swap(from_asset, _, swap_data) => { + let has_swap_memo = has_swap_quote_memo(has_memo, &swap_data.data); + match (&from_asset.id.token_id, &swap_data.data.data_type) { + (_, SwapQuoteDataType::Contract) if !swap_data.data.data.is_empty() => { + self.estimate_swap_contract_fee(&input.sender_address, &swap_data.data, &chain_parameters, &account_usage) + .await? + } + (None, _) => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_swap_memo)?), + (Some(token_id), SwapQuoteDataType::Transfer) => { + self.estimate_token_transfer_fee( + input.sender_address.clone(), + swap_data.data.to.clone(), + token_id.clone(), + input.value.clone(), + &chain_parameters, + &account_usage, + ) .await? + } + (Some(_), SwapQuoteDataType::Contract) => return Err("Tron token contract swap calldata is required".into()), } - (None, _) => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_memo)?), - (Some(token_id), SwapQuoteDataType::Transfer) => { - self.estimate_token_transfer_fee( - input.sender_address.clone(), - swap_data.data.to.clone(), - token_id.clone(), - input.value.clone(), - &chain_parameters, - &account_usage, - ) - .await? - } - (Some(_), SwapQuoteDataType::Contract) => return Err("Tron token contract swap calldata is required".into()), - }, + } _ => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_memo)?), }; @@ -103,6 +106,10 @@ impl ChainTransactionLoad for TronClient { } } +fn has_swap_quote_memo(input_has_memo: bool, data: &SwapQuoteData) -> bool { + input_has_memo || data.memo.as_deref().is_some_and(|memo| !memo.is_empty()) +} + impl TronClient { async fn estimate_token_transfer_fee( &self, @@ -198,6 +205,25 @@ impl TronClient { } } +#[cfg(test)] +mod tests { + use super::has_swap_quote_memo; + use primitives::swap::SwapQuoteData; + + #[test] + fn test_swap_quote_memo_counts_for_fee_preload() { + let mut data = SwapQuoteData::new_contract("TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi".to_string(), "0".to_string(), String::new(), None, None); + assert!(!has_swap_quote_memo(false, &data)); + + data.memo = Some(String::new()); + assert!(!has_swap_quote_memo(false, &data)); + + data.memo = Some("0x0100".to_string()); + assert!(has_swap_quote_memo(false, &data)); + assert!(has_swap_quote_memo(true, &data)); + } +} + #[cfg(all(test, feature = "chain_integration_tests"))] mod chain_integration_tests { use super::*; diff --git a/core/crates/primitives/src/testkit/swap_mock.rs b/core/crates/primitives/src/testkit/swap_mock.rs index cba6750c11..7faeeb06ec 100644 --- a/core/crates/primitives/src/testkit/swap_mock.rs +++ b/core/crates/primitives/src/testkit/swap_mock.rs @@ -95,13 +95,6 @@ impl SwapData { ..swap_data } } - - pub fn mock_transfer_with_addresses(provider: SwapProvider, from_address: &str, from_value: &str, to_address: &str, to_value: &str, to: &str) -> Self { - SwapData { - quote: SwapQuote::mock_with_addresses(provider, from_address, from_value, to_address, to_value), - data: SwapQuoteData::new_tranfer(to.to_string(), from_value.to_string(), None), - } - } } impl SwapQuote { diff --git a/core/crates/swapper/src/chainflip/provider.rs b/core/crates/swapper/src/chainflip/provider.rs index 962785b022..258e75e2e3 100644 --- a/core/crates/swapper/src/chainflip/provider.rs +++ b/core/crates/swapper/src/chainflip/provider.rs @@ -28,13 +28,14 @@ use crate::{ solana::DEFAULT_SWAP_GAS_LIMIT, }; use primitives::{ - Asset, - ChainType, + Asset, ChainType, chain::Chain, swap::{QuoteAsset, SwapQuoteDataType::Contract}, }; const DEFAULT_SWAP_ERC20_GAS_LIMIT: u64 = 100_000; +const CHAINFLIP_EVM_REFUND_RETRY_BLOCKS: u32 = 150; +const CHAINFLIP_DEFAULT_REFUND_RETRY_BLOCKS: u32 = 10; const VAULT_ETH: &str = "0xF5e10380213880111522dd0efD3dbb45b9f62Bcc"; const VAULT_ARB: &str = "0x79001a5e762f3bEFC8e5871b42F6734e00498920"; @@ -108,14 +109,22 @@ fn build_quote_request(request: &QuoteRequest) -> Result Option { +fn map_tron_quote_data(response: &TronVaultSwapResponse) -> Result { let address = response.source_token_address.as_deref().unwrap_or(&response.to); + let to = TronAddress::from_hex_or_base58(address) + .map(|address| address.to_string()) + .ok_or_else(|| SwapperError::TransactionError(format!("invalid Tron address: {address}")))?; + let data = if response.calldata.eq_ignore_ascii_case("0x") { + String::new() + } else { + response.calldata.clone() + }; - Some(SwapperQuoteData { - to: TronAddress::from_hex(address).map(|address| address.to_string())?, + Ok(SwapperQuoteData { + to, data_type: Contract, value: response.value.to_string(), - data: response.calldata.clone(), + data, memo: Some(response.note.clone()), approval: None, gas_limit: None, @@ -257,7 +266,7 @@ where chain, input_amount: input_amount.clone(), refund_parameters: RefundParameters { - retry_duration: 150, + retry_duration: CHAINFLIP_EVM_REFUND_RETRY_BLOCKS, refund_address: quote.request.wallet_address.clone(), min_price, }, @@ -266,7 +275,7 @@ where chain, input_amount: input_amount.clone(), refund_parameters: RefundParameters { - retry_duration: 10, + retry_duration: CHAINFLIP_DEFAULT_REFUND_RETRY_BLOCKS, refund_address: quote.request.wallet_address.clone(), min_price, }, @@ -277,7 +286,7 @@ where chain, input_amount: input_amount.to_u64().unwrap(), refund_parameters: RefundParameters { - retry_duration: 10, + retry_duration: CHAINFLIP_DEFAULT_REFUND_RETRY_BLOCKS, refund_address: quote.request.wallet_address.clone(), min_price, }, @@ -321,10 +330,7 @@ where Ok(SwapperQuoteData::new_contract(response.to, value, response.calldata, approval, gas_limit)) } - VaultSwapResponse::Tron(response) => { - let address = response.source_token_address.as_deref().unwrap_or(&response.to); - map_tron_quote_data(&response).ok_or_else(|| SwapperError::TransactionError(format!("invalid Tron address: {address}"))) - } + VaultSwapResponse::Tron(response) => map_tron_quote_data(&response), VaultSwapResponse::Solana(response) => { let data = tx_builder::build_solana_tx("e.request.wallet_address, &response, self.rpc_provider.clone()) .await @@ -355,7 +361,22 @@ where mod tests { use super::*; use crate::SwapperQuoteAsset; - use primitives::AssetId; + use primitives::{ + AssetId, + swap::{SwapQuoteDataType, SwapQuoteDataType::Transfer}, + }; + + #[cfg(feature = "swap_integration_tests")] + use crate::NativeProvider; + #[cfg(feature = "swap_integration_tests")] + use primitives::swap::SwapStatus; + + fn assert_contract_quote_data(data_type: SwapQuoteDataType) { + match data_type { + Contract => {} + Transfer => panic!("expected contract quote data"), + } + } #[test] fn test_chainflip_min_amount_error() { @@ -432,7 +453,7 @@ mod tests { } #[test] - fn test_tron_quote_data_maps_contract_call_fields() { + fn test_tron_quote_data_maps_trc20_contract_call_fields() { let note = "0x0300"; let data = map_tron_quote_data(&TronVaultSwapResponse { calldata: "0xa9059cbb".to_string(), @@ -443,19 +464,44 @@ mod tests { }) .unwrap(); - assert!(matches!(data.data_type, Contract)); + assert_contract_quote_data(data.data_type); assert_eq!(data.to, "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"); assert_eq!(data.memo, Some(note.to_string())); assert_eq!(data.value, "0"); assert_eq!(data.data, "0xa9059cbb"); + + let data = map_tron_quote_data(&TronVaultSwapResponse { + calldata: "0xa9059cbb".to_string(), + value: BigUint::from(0u32), + to: "TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi".to_string(), + note: note.to_string(), + source_token_address: Some("TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf".to_string()), + }) + .unwrap(); + + assert_eq!(data.to, "TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf"); + } + + #[test] + fn test_tron_quote_data_maps_native_contract_transfer_fields() { + let note = "0x0300"; + let data = map_tron_quote_data(&TronVaultSwapResponse { + calldata: "0x".to_string(), + value: BigUint::from(50_000_000u32), + to: "TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi".to_string(), + note: note.to_string(), + source_token_address: None, + }) + .unwrap(); + + assert_eq!(data.to, "TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi"); + assert_eq!(data.value, "50000000"); + assert_eq!(data.data, ""); } #[tokio::test] #[cfg(feature = "swap_integration_tests")] async fn test_get_swap_result() -> Result<(), Box> { - use crate::alien::reqwest_provider::NativeProvider; - use primitives::swap::SwapStatus; - let network_provider = Arc::new(NativeProvider::default()); let swap_provider = ChainflipProvider::new(network_provider.clone());