Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions android/justfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ run-device: build
_run-adb TARGET:
#!/usr/bin/env bash
set -euo pipefail
PACKAGE="com.gemwallet.android"
if [ "{{TARGET}}" = "emulator" ]; then
DEVICE=$(adb devices | awk '$2=="device" && $1 ~ /^emulator-/ {print $1; exit}')
[ -z "$DEVICE" ] && { echo "No emulator found (try 'just start-emulator')"; exit 1; }
Expand All @@ -44,16 +45,34 @@ _run-adb TARGET:
echo "==> Installing on $DEVICE..."
adb -s "$DEVICE" install -r "$APK"
echo "==> Launching app..."
adb -s "$DEVICE" shell am start -n com.gemwallet.android/com.gemwallet.android.MainActivity
adb -s "$DEVICE" shell am start -W -n "$PACKAGE/$PACKAGE.MainActivity"
echo "==> Streaming logs (Ctrl+C to stop)..."
sleep 1
PID=$(adb -s "$DEVICE" shell pidof com.gemwallet.android)
PID=""
for _ in {1..60}; do
PID=$(adb -s "$DEVICE" shell pidof -s "$PACKAGE" | tr -d '\r' || true)
[ -n "$PID" ] && break
sleep 0.5
done
if [ -z "$PID" ]; then
echo "App not running, showing unfiltered logcat"
adb -s "$DEVICE" logcat
else
adb -s "$DEVICE" logcat --pid="$PID"
echo "App process not found after launch; showing recent app errors"
adb -s "$DEVICE" logcat -d -t 500 | grep -E "$PACKAGE|AndroidRuntime|FATAL EXCEPTION" || true
exit 1
fi
trap 'echo; echo "==> Log streaming stopped."; exit 0' INT TERM
set +e
adb -s "$DEVICE" logcat --pid="$PID"
STATUS=$?
set -e
case "$STATUS" in
0|130|143)
echo
echo "==> Log streaming stopped."
exit 0
;;
*)
exit "$STATUS"
;;
esac

build-test:
@./gradlew assembleGoogleDebugAndroidTest
Expand Down
1 change: 1 addition & 0 deletions core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 23 additions & 6 deletions core/crates/gem_tron/src/address/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ pub struct TronAddress([u8; PREFIXED_ADDRESS_LEN]);
impl TronAddress {
pub fn from_hex(hex_value: &str) -> Option<Self> {
let bytes = decode_hex(hex_value).ok()?;
if bytes.len() != PREFIXED_ADDRESS_LEN || bytes.first() != Some(&ADDRESS_PREFIX) {
return None;
match bytes.len() {
PREFIXED_ADDRESS_LEN if bytes.first() == Some(&ADDRESS_PREFIX) => Some(Self(bytes.try_into().ok()?)),
ADDRESS_LEN => bytes.try_into().ok().map(|account_id: [u8; ADDRESS_LEN]| Self::from(account_id)),
_ => None,
}
Some(Self(bytes.try_into().ok()?))
}

#[cfg(feature = "signer")]
pub(crate) fn from_hex_or_base58(value: &str) -> Option<Self> {
pub fn from_hex_or_base58(value: &str) -> Option<Self> {
// v3-compatible raw transaction parsing prefers base58 when both formats are technically valid.
Self::try_parse(value).or_else(|| Self::from_hex(value))
}
Expand Down Expand Up @@ -123,6 +123,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);
}

Expand Down Expand Up @@ -155,13 +159,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);
}

Expand Down
4 changes: 2 additions & 2 deletions core/crates/gem_tron/src/models/signing/raw_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub(crate) struct TronRawData {
}

impl TronRawData {
pub(crate) fn from_input(input: &SignerInput, contract: TronContract, fee_limit: u64) -> Result<Self, SignerError> {
pub(crate) fn from_input_with_data(input: &SignerInput, contract: TronContract, fee_limit: u64, data: Option<Vec<u8>>) -> Result<Self, SignerError> {
let TransactionLoadMetadata::Tron {
block_number,
block_version,
Expand Down Expand Up @@ -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,
Expand Down
181 changes: 159 additions & 22 deletions core/crates/gem_tron/src/provider/preload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ use num_bigint::BigInt;
use gem_client::Client;
use number_formatter::BigNumberFormatter;
use primitives::{
AssetSubtype, FeePriority, FeeRate, GasPriceType, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata,
TransactionPreloadInput, TransferDataOutputAction, TronStakeData,
Asset, AssetSubtype, FeePriority, FeeRate, GasPriceType, TransactionFee, TransactionInputType, TransactionLoadData, TransactionLoadInput, TransactionLoadMetadata,
TransactionPreloadInput, TransferDataOutputAction, TronStakeData, decode_hex,
swap::{SwapData, SwapQuoteData, SwapQuoteDataType},
};

use crate::{
address::TronAddress,
models::{ChainParameter, TriggerSmartContractData, account::TronAccountUsage},
provider::preload_mapper::{calculate_stake_fee_rate, calculate_transfer_fee_rate, calculate_transfer_token_fee_rate, map_stake_data},
provider::preload_mapper::{
FeeEstimateContext, calculate_stake_fee_rate, calculate_transfer_token_fee_rate, calculate_transfer_token_fee_rate_with_data, map_stake_data, native_transfer_fee,
},
rpc::client::TronClient,
};

Expand Down Expand Up @@ -45,10 +48,15 @@ impl<C: Client> ChainTransactionLoad for TronClient<C> {
stake_data,
};

let fee_context = FeeEstimateContext {
chain_parameters: &chain_parameters,
account_usage: &account_usage,
is_new_account,
};
let has_memo = input.get_memo().is_some();
let fee = match &input.input_type {
TransactionInputType::Transfer(asset) | TransactionInputType::TransferNft(asset, _) | TransactionInputType::Account(asset, _) => match &asset.id.token_id {
None => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_memo)?),
None => native_transfer_fee(&fee_context, has_memo)?,
Some(token_id) => {
self.estimate_token_transfer_fee(
input.sender_address.clone(),
Expand All @@ -57,6 +65,7 @@ impl<C: Client> ChainTransactionLoad for TronClient<C> {
input.value.clone(),
&chain_parameters,
&account_usage,
input.get_memo().map(|memo| memo.len() as u64),
)
.await?
}
Expand All @@ -67,26 +76,13 @@ impl<C: Client> ChainTransactionLoad for TronClient<C> {
.await?
{
Some(fee) => fee,
None => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_memo)?),
None => native_transfer_fee(&fee_context, has_memo)?,
},
TransferDataOutputAction::Sign => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, false)?),
TransferDataOutputAction::Sign => native_transfer_fee(&fee_context, 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) => {
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?
}
},
_ => TransactionFee::new_from_fee(calculate_transfer_fee_rate(&chain_parameters, &account_usage, is_new_account, has_memo)?),
TransactionInputType::Swap(from_asset, _, swap_data) => self.estimate_swap_fee(&input, from_asset, swap_data, &fee_context, input.get_memo()).await?,
_ => native_transfer_fee(&fee_context, has_memo)?,
};

Ok(TransactionLoadData { fee, metadata })
Expand All @@ -97,7 +93,80 @@ impl<C: Client> ChainTransactionLoad for TronClient<C> {
}
}

fn has_swap_quote_memo(input_memo: Option<&str>, data: &SwapQuoteData) -> bool {
input_memo.is_some() || data.memo.as_deref().is_some_and(|memo| !memo.is_empty())
}

fn swap_contract_memo_data_bytes(input_memo: Option<&str>, data: &SwapQuoteData) -> Result<Option<u64>, Box<dyn Error + Send + Sync>> {
if let Some(memo) = data.memo.as_deref().filter(|memo| !memo.is_empty()) {
let bytes = decode_hex(memo).map_err(|_| "invalid Tron swap memo")?;
return Ok(Some(bytes.len() as u64));
}

Ok(input_memo.map(|memo| memo.len() as u64))
}

impl<C: Client> TronClient<C> {
async fn estimate_swap_fee(
&self,
input: &TransactionLoadInput,
from_asset: &Asset,
swap_data: &SwapData,
fee_context: &FeeEstimateContext<'_>,
input_memo: Option<&str>,
) -> Result<TransactionFee, Box<dyn Error + Send + Sync>> {
match &swap_data.data.data_type {
SwapQuoteDataType::Contract => self.estimate_contract_swap_fee(&input.sender_address, from_asset, swap_data, fee_context, input_memo).await,
SwapQuoteDataType::Transfer => self.estimate_transfer_swap_fee(input, from_asset, swap_data, fee_context, input_memo).await,
}
}

async fn estimate_contract_swap_fee(
&self,
sender_address: &str,
from_asset: &Asset,
swap_data: &SwapData,
fee_context: &FeeEstimateContext<'_>,
input_memo: Option<&str>,
) -> Result<TransactionFee, Box<dyn Error + Send + Sync>> {
if !swap_data.data.data.is_empty() {
let memo_data_bytes = swap_contract_memo_data_bytes(input_memo, &swap_data.data)?;
return self
.estimate_contract_call_fee(sender_address, &swap_data.data, fee_context.chain_parameters, fee_context.account_usage, memo_data_bytes)
.await;
}
if from_asset.id.token_id.is_some() {
return Err("Tron token contract swap calldata is required".into());
}

native_transfer_fee(fee_context, has_swap_quote_memo(input_memo, &swap_data.data))
}

async fn estimate_transfer_swap_fee(
&self,
input: &TransactionLoadInput,
from_asset: &Asset,
swap_data: &SwapData,
fee_context: &FeeEstimateContext<'_>,
input_memo: Option<&str>,
) -> Result<TransactionFee, Box<dyn Error + Send + Sync>> {
match &from_asset.id.token_id {
None => native_transfer_fee(fee_context, has_swap_quote_memo(input_memo, &swap_data.data)),
Some(token_id) => {
self.estimate_token_transfer_fee(
input.sender_address.clone(),
swap_data.data.to.clone(),
token_id.clone(),
input.value.clone(),
fee_context.chain_parameters,
fee_context.account_usage,
input_memo.map(|memo| memo.len() as u64),
)
.await
}
}
}

async fn estimate_token_transfer_fee(
&self,
sender_address: String,
Expand All @@ -106,10 +175,49 @@ impl<C: Client> TronClient<C> {
value: String,
chain_parameters: &[ChainParameter],
account_usage: &TronAccountUsage,
memo_data_bytes: Option<u64>,
) -> Result<TransactionFee, Box<dyn Error + Send + Sync>> {
let destination_parameter = TronAddress::parse(&destination_address)?.abi_address_parameter();
let estimated_energy = self.estimate_trc20_transfer_gas(sender_address, token_id, destination_parameter, value).await?;
let token_fee = calculate_transfer_token_fee_rate(chain_parameters, account_usage, estimated_energy)?;
let token_fee = calculate_transfer_token_fee_rate_with_data(
chain_parameters,
account_usage,
estimated_energy,
memo_data_bytes.unwrap_or_default(),
memo_data_bytes.is_some(),
)?;

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_contract_call_fee(
&self,
sender_address: &str,
data: &SwapQuoteData,
chain_parameters: &[ChainParameter],
account_usage: &TronAccountUsage,
memo_data_bytes: Option<u64>,
) -> Result<TransactionFee, Box<dyn Error + Send + Sync>> {
let contract_data = TriggerSmartContractData {
contract_address: data.to.clone(),
data: data.data.clone(),
owner_address: sender_address.to_string(),
fee_limit: None,
call_value: Some(data.value.parse::<u64>()?).filter(|value| *value > 0),
};
let estimated_energy = self.estimate_energy_with_data(&contract_data).await?;
let token_fee = calculate_transfer_token_fee_rate_with_data(
chain_parameters,
account_usage,
estimated_energy,
memo_data_bytes.unwrap_or_default(),
memo_data_bytes.is_some(),
)?;

Ok(TransactionFee::new_gas_price_type(
GasPriceType::regular(BigInt::from(token_fee.energy_price)),
Expand Down Expand Up @@ -167,6 +275,35 @@ impl<C: Client> TronClient<C> {
}
}

#[cfg(test)]
mod tests {
use super::{has_swap_quote_memo, swap_contract_memo_data_bytes};
use primitives::swap::SwapQuoteData;

#[test]
fn test_swap_memo_fee_preload_uses_quote_memo_bytes() {
let mut data = SwapQuoteData::new_contract("TDMakP1fbWc7XXoSWZpujpjRAuePPEn4oi".to_string(), "0".to_string(), String::new(), None, None);

assert!(!has_swap_quote_memo(None, &data));
assert_eq!(swap_contract_memo_data_bytes(None, &data).unwrap(), None);

assert!(has_swap_quote_memo(Some("memo"), &data));
assert_eq!(swap_contract_memo_data_bytes(Some("memo"), &data).unwrap(), Some(4));

data.memo = Some(String::new());
assert!(!has_swap_quote_memo(None, &data));
assert_eq!(swap_contract_memo_data_bytes(None, &data).unwrap(), None);

data.memo = Some("0x010203".to_string());
assert!(has_swap_quote_memo(None, &data));
assert!(has_swap_quote_memo(Some("memo"), &data));
assert_eq!(swap_contract_memo_data_bytes(Some("memo"), &data).unwrap(), Some(3));

data.memo = Some("0xzz".to_string());
assert_eq!(swap_contract_memo_data_bytes(None, &data).unwrap_err().to_string(), "invalid Tron swap memo");
}
}

#[cfg(all(test, feature = "chain_integration_tests"))]
mod chain_integration_tests {
use super::*;
Expand Down
Loading
Loading