Skip to content
Draft
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
34 changes: 34 additions & 0 deletions app/src/main/java/one/mixin/android/crypto/CryptoWalletHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ object CryptoWalletHelper {
.removeSuffix("'/0'")
.toIntOrNull()
}
// Tron path: m/44'/195'/0'/0/{index}
path.startsWith("m/44'/195'/") -> {
path.removePrefix("m/44'/195'/0'/0/").toIntOrNull()
}
// Bitcoin SegWit path: m/84'/0'/0'/0/{index}
path.startsWith("m/84'/0'/") -> {
path.removePrefix("m/84'/0'/0'/0/").toIntOrNull()
Expand Down Expand Up @@ -162,6 +166,21 @@ object CryptoWalletHelper {
}
}

fun mnemonicToTronWallet(mnemonic: String, passphrase: String = "", index: Int = 0): CryptoWallet {
try {
val path = Bip44Path.tronPathString(index)
val privateKey = TronKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, passphrase, index)
return CryptoWallet(
mnemonic = mnemonic,
privateKey = Numeric.toHexString(privateKey),
address = TronKeyGenerator.privateKeyToAddress(privateKey),
path = path,
)
} catch (e: Exception) {
throw RuntimeException("Tron wallet generation failed: ${e.message}", e)
}
}

fun mnemonicToEthereumWallet(mnemonic: String, passphrase: String = "", index: Int = 0): CryptoWallet {
try {
val path = Bip44Path.ethereumPathString(index)
Expand Down Expand Up @@ -214,6 +233,10 @@ object CryptoWalletHelper {
UtxoKeyGenerator.privateKeyToAddress(privateKey, chainId)
}

Constants.ChainId.TRON_CHAIN_ID -> {
TronKeyGenerator.privateKeyToAddress(Numeric.hexStringToByteArray(privateKey))
}

in Constants.Web3EvmChainIds -> {
val privateKeyBytes: ByteArray = Numeric.hexStringToByteArray(privateKey)
EthKeyGenerator.privateKeyToAddress(privateKeyBytes)
Expand All @@ -240,6 +263,10 @@ object CryptoWalletHelper {
Constants.ChainId.PEARL_CHAIN_ID -> {
UtxoKeyGenerator.mnemonicToAddress(mnemonic, chainId, passphrase, index)
}
Constants.ChainId.TRON_CHAIN_ID -> {
val privateKey = TronKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, passphrase, index)
TronKeyGenerator.privateKeyToAddress(privateKey)
}
in Constants.Web3EvmChainIds -> {
val privateKey: ByteArray =
EthKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, passphrase, index)
Expand Down Expand Up @@ -276,6 +303,7 @@ object CryptoWalletHelper {
)
ECKey.fromPrivate(privateKey, true).getPrivateKeyEncoded(BitcoinNetwork.MAINNET).toBase58()
}
Constants.ChainId.TRON_CHAIN_ID -> mnemonicToTronWallet(mnemonic, index = index).privateKey
in Constants.Web3EvmChainIds -> mnemonicToEthereumWallet(mnemonic, index = index).privateKey
else -> throw IllegalArgumentException("Unsupported chainId: $chainId")
}
Expand Down Expand Up @@ -373,6 +401,9 @@ object CryptoWalletHelper {
Numeric.hexStringToByteArray(privateKeyStr)
}
}
Constants.ChainId.TRON_CHAIN_ID -> {
Numeric.hexStringToByteArray(privateKeyStr)
}
in Constants.Web3EvmChainIds -> {
Numeric.hexStringToByteArray(privateKeyStr)
}
Expand Down Expand Up @@ -448,6 +479,9 @@ object CryptoWalletHelper {
index = derivationIndex,
)
}
Constants.ChainId.TRON_CHAIN_ID -> {
TronKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, index = derivationIndex)
}
in Constants.Web3EvmChainIds -> {
EthKeyGenerator.getPrivateKeyFromMnemonic(mnemonic, index = derivationIndex)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal object TipKeyGenerator {
Constants.ChainId.BITCOIN_CHAIN_ID,
Constants.ChainId.PEARL_CHAIN_ID -> UtxoKeyGenerator.deriveFromTipSeed(seed, chainId, index)
Constants.ChainId.SOLANA_CHAIN_ID -> SolanaKeyGenerator.deriveFromTipSeed(seed, index)
Constants.ChainId.TRON_CHAIN_ID -> TronKeyGenerator.deriveFromTipSeed(seed, index)
in Constants.Web3EvmChainIds -> EthKeyGenerator.deriveFromTipSeed(seed, index)
else -> throw IllegalArgumentException("Not supported chainId")
}
Expand Down
93 changes: 93 additions & 0 deletions app/src/main/java/one/mixin/android/crypto/TronKeyGenerator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package one.mixin.android.crypto

import one.mixin.android.tip.bip44.Bip44Path
import one.mixin.android.tip.bip44.generateBip44Key
import one.mixin.android.util.encodeToBase58WithChecksum
import org.bitcoinj.crypto.MnemonicCode
import org.json.JSONArray
import org.json.JSONObject
import org.web3j.crypto.Bip32ECKeyPair
import org.web3j.crypto.ECKeyPair
import org.web3j.crypto.Hash
import org.web3j.crypto.MnemonicUtils
import org.web3j.crypto.Sign
import org.web3j.utils.Numeric
import java.nio.charset.StandardCharsets

object TronKeyGenerator {
private const val ADDRESS_PREFIX = 0x41
private const val MESSAGE_PREFIX = "\u0019TRON Signed Message:\n"

fun getPrivateKeyFromMnemonic(
mnemonic: String,
passphrase: String = "",
index: Int = 0,
): ByteArray {
val words = mnemonic.split(" ")
MnemonicCode.INSTANCE.check(words)
val seed = MnemonicUtils.generateSeed(mnemonic, passphrase)
val masterKeyPair = Bip32ECKeyPair.generateKeyPair(seed)
val keyPair = generateBip44Key(masterKeyPair, Bip44Path.tron(index))
return Numeric.toBytesPadded(keyPair.privateKey, 32)
}

fun privateKeyToAddress(privateKey: ByteArray): String {
return addressPayload(privateKey).encodeToBase58WithChecksum()
}

internal fun deriveFromTipSeed(seed: ByteArray, index: Int): TipDerivedKey {
val masterKeyPair = Bip32ECKeyPair.generateKeyPair(seed)
val keyPair = generateBip44Key(masterKeyPair, Bip44Path.tron(index))
val privateKey = Numeric.toBytesPadded(keyPair.privateKey, 32)
return TipDerivedKey(privateKey, privateKeyToAddress(privateKey))
}

fun signMessageV2(privateKey: ByteArray, message: String): String {
return signMessageV2(privateKey, message.toByteArray(StandardCharsets.UTF_8))
}

fun signMessageV2(privateKey: ByteArray, messageBytes: ByteArray): String {
val prefix = MESSAGE_PREFIX.toByteArray(StandardCharsets.UTF_8)
val size = messageBytes.size.toString().toByteArray(StandardCharsets.UTF_8)
return Numeric.toHexString(signDigest(privateKey, Hash.sha3(prefix + size + messageBytes)))
}

fun signTransaction(privateKey: ByteArray, transactionJson: String): String {
val transaction = JSONObject(transactionJson)
val txId = Numeric.hexStringToByteArray(transaction.getString("txID"))
require(txId.size == 32) { "Tron txID must be 32 bytes" }
val contracts = transaction.getJSONObject("raw_data").getJSONArray("contract")
require(contracts.length() > 0) { "Tron transaction has no contract" }
val ownerAddress = contracts.getJSONObject(0)
.getJSONObject("parameter")
.getJSONObject("value")
.getString("owner_address")
val expectedHexAddress = Numeric.toHexStringNoPrefix(addressPayload(privateKey))
require(
ownerAddress.equals(privateKeyToAddress(privateKey), ignoreCase = true) ||
Numeric.cleanHexPrefix(ownerAddress).equals(expectedHexAddress, ignoreCase = true)
) { "Tron transaction owner does not match the selected wallet" }
val signature = Numeric.toHexStringNoPrefix(signDigest(privateKey, txId))
val signatures = transaction.optJSONArray("signature") ?: JSONArray().also {
transaction.put("signature", it)
}
if ((0 until signatures.length()).none { signatures.optString(it).equals(signature, ignoreCase = true) }) {
signatures.put(signature)
}
return transaction.toString()
}

private fun addressPayload(privateKey: ByteArray): ByteArray {
require(privateKey.size == 32) { "Tron private key must be 32 bytes" }
val publicKey = Numeric.toBytesPadded(ECKeyPair.create(privateKey).publicKey, 64)
val hash = Hash.sha3(publicKey)
return byteArrayOf(ADDRESS_PREFIX.toByte()) + hash.copyOfRange(hash.size - 20, hash.size)
}

private fun signDigest(privateKey: ByteArray, digest: ByteArray): ByteArray {
require(privateKey.size == 32) { "Tron private key must be 32 bytes" }
require(digest.size == 32) { "Tron digest must be 32 bytes" }
val signature = Sign.signMessage(digest, ECKeyPair.create(privateKey), false)
return signature.r + signature.s + signature.v
}
}
17 changes: 17 additions & 0 deletions app/src/main/java/one/mixin/android/tip/bip44/Bip44.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,19 @@ object Bip44Path {
0 or HARDENED_BIT,
)

/**
* Generate Tron derivation path with variable index
* Tron path: m/44'/195'/0'/0/{index}
*/
fun tron(index: Int = 0): IntArray =
intArrayOf(
44 or HARDENED_BIT,
195 or HARDENED_BIT,
0 or HARDENED_BIT,
0,
index,
)

fun ethereumPathString(index: Int = 0): String {
return "m/44'/60'/0'/0/$index"
}
Expand All @@ -79,6 +92,10 @@ object Bip44Path {
return "m/44'/501'/${index}'/0'"
}

fun tronPathString(index: Int = 0): String {
return "m/44'/195'/0'/0/$index"
}

fun bitcoinSegwitPathString(index: Int = 0): String {
return "m/84'/0'/0'/0/$index"
}
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/one/mixin/android/tip/wc/internal/Chain.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import one.mixin.android.Constants
import one.mixin.android.Constants.ChainId.BITCOIN_CHAIN_ID
import one.mixin.android.Constants.ChainId.ETHEREUM_CHAIN_ID
import one.mixin.android.Constants.ChainId.SOLANA_CHAIN_ID
import one.mixin.android.Constants.ChainId.TRON_CHAIN_ID
import one.mixin.android.MixinApplication
import one.mixin.android.extension.defaultSharedPreferences

Expand Down Expand Up @@ -47,6 +48,8 @@ sealed class Chain(

object Bitcoin : Chain(BITCOIN_CHAIN_ID, "bip122", "000000000019d6689c085ae165831e93", "000000000019d6689c085ae165831e93", "Bitcoin", "BTC", listOf(""))

object Tron : Chain(TRON_CHAIN_ID, "tron", "mainnet", "mainnet", "Tron", "TRX", listOf("https://api.trongrid.io"))

val chainId: String
get() {
return "$chainNamespace:$chainReference"
Expand Down Expand Up @@ -75,6 +78,7 @@ sealed class Chain(
HyperEVM -> Constants.ChainId.HyperEVM
Solana -> Constants.ChainId.Solana
Bitcoin -> BITCOIN_CHAIN_ID
Tron -> TRON_CHAIN_ID
}
}
// Chain.Blast
Expand All @@ -91,6 +95,7 @@ internal fun WalletConnectAddresses.accountFor(chain: Chain): String =
when (chain) {
Chain.Solana -> solana
Chain.Bitcoin -> bitcoin
Chain.Tron -> ""
else -> evm
}

Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/one/mixin/android/ui/home/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -905,9 +905,10 @@ class MainActivity : BlazeBaseActivity(), WalletMissingBtcAddressFragment.Callba
}

private suspend fun initWalletConnect() {
if (!WalletConnect.isEnabled()) return
try {
WalletConnectV2
if (WalletConnect.isEnabled()) {
WalletConnectV2
}
val classicWalletId = web3Repository.getClassicWalletId()
Web3Signer.init(
{ classicWalletId },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import one.mixin.android.R
import one.mixin.android.api.request.web3.EstimateFeeRequest
import one.mixin.android.api.response.web3.ParsedTx
import one.mixin.android.api.response.web3.WalletOutput
import one.mixin.android.crypto.TronKeyGenerator
import one.mixin.android.db.web3.vo.Web3TokenItem
import one.mixin.android.db.web3.vo.getChainFromName
import one.mixin.android.extension.base64Encode
Expand Down Expand Up @@ -154,7 +155,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
}
private val isFeeWaived by lazy { requireArguments().getBoolean(ARGS_IS_FEE_FREE, false) }
private val currentChain by lazy {
token?.getChainFromName() ?: Web3Signer.currentChain
if (signMessage.isTronMessage()) Chain.Tron else token?.getChainFromName() ?: Web3Signer.currentChain
}
private val utxoChainId: String
get() = signMessage.utxoChainId?.takeIf { it in Constants.Web3UtxoChainIds }
Expand Down Expand Up @@ -192,6 +193,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
if (address.isBlank()) {
lifecycleScope.launch {
address = when {
signMessage.isTronMessage() -> Web3Signer.tronAddress
signMessage.type == JsSignMessage.TYPE_MESSAGE -> Web3Signer.address
signMessage.isEvmMessage() -> Web3Signer.evmAddress
signMessage.isSolMessage() -> Web3Signer.solanaAddress
Expand Down Expand Up @@ -303,6 +305,12 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
}

private fun refreshEstimatedGasAndAsset(chain: Chain) {
if (signMessage.isTronMessage()) {
lifecycleScope.launch {
asset = viewModel.refreshAsset(Constants.ChainId.TRON_CHAIN_ID)
}
return
}
if (signMessage.isGaslessTransfer()) {
lifecycleScope.launch {
asset = viewModel.refreshAsset(feeToken?.assetId ?: chain.getWeb3ChainId())
Expand Down Expand Up @@ -406,7 +414,21 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
}
return@launch
}
if (signMessage.type == JsSignMessage.TYPE_UTXO_TRANSACTION) {
if (signMessage.type == JsSignMessage.TYPE_TRON_TRANSACTION) {
val priv = viewModel.getWeb3Priv(requireContext(), pin, Constants.ChainId.TRON_CHAIN_ID)
val signedTransaction = TronKeyGenerator.signTransaction(priv, requireNotNull(signMessage.data))
settleJsonSuccess(signedTransaction)
} else if (signMessage.type == JsSignMessage.TYPE_TRON_MESSAGE) {
val priv = viewModel.getWeb3Priv(requireContext(), pin, Constants.ChainId.TRON_CHAIN_ID)
require(
TronKeyGenerator.privateKeyToAddress(priv).equals(Web3Signer.tronAddress, ignoreCase = true)
) { "Tron signing key does not match the selected wallet" }
val signature = TronKeyGenerator.signMessageV2(
priv,
signMessage.tronMessageBytes ?: requireNotNull(signMessage.data).toByteArray(Charsets.UTF_8),
)
settleSuccess(signature)
} else if (signMessage.type == JsSignMessage.TYPE_UTXO_TRANSACTION) {
val rawHex = signMessage.data ?: throw IllegalArgumentException("empty UTXO transaction hex")
val chainId = utxoChainId
val priv = viewModel.getWeb3Priv(requireContext(), pin, chainId)
Expand Down Expand Up @@ -533,6 +555,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag

private fun isAccountUnavailable(): Boolean =
when {
signMessage.isTronMessage() -> Web3Signer.tronAddress.isBlank()
signMessage.type == JsSignMessage.TYPE_MESSAGE -> Web3Signer.address.isBlank()
signMessage.isSolMessage() || (signMessage.isGaslessTransfer() && currentChain == Chain.Solana) -> Web3Signer.solanaAddress.isBlank()
signMessage.isEvmMessage() || (signMessage.isGaslessTransfer() && currentChain != Chain.Solana) -> Web3Signer.evmAddress.isBlank()
Expand All @@ -545,6 +568,12 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag
)
}

private fun settleJsonSuccess(result: String) {
settleRequest(
"mixinwallet.${Web3Signer.currentNetwork}.sendResponse(${signMessage.callbackId}, $result);",
)
}

private fun settleError(
code: Int,
message: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ suspend fun buildClassicWalletRequest(
val name = nextCommonWalletName(names)
val evmAddress = privateKeyToAddress(spendKey, Constants.ChainId.ETHEREUM_CHAIN_ID, classicIndex)
val solAddress = privateKeyToAddress(spendKey, Constants.ChainId.SOLANA_CHAIN_ID, classicIndex)
val tronAddress = privateKeyToAddress(spendKey, Constants.ChainId.TRON_CHAIN_ID, classicIndex)
return WalletRequest(
name = name,
category = WalletCategory.CLASSIC.value,
Expand All @@ -75,6 +76,13 @@ suspend fun buildClassicWalletRequest(
privateKey = tipPrivToPrivateKey(spendKey, Constants.ChainId.SOLANA_CHAIN_ID, classicIndex),
category = WalletCategory.CLASSIC.value,
))
add(createSignedWeb3AddressRequest(
destination = tronAddress,
chainId = Constants.ChainId.TRON_CHAIN_ID,
path = Bip44Path.tronPathString(classicIndex),
privateKey = tipPrivToPrivateKey(spendKey, Constants.ChainId.TRON_CHAIN_ID, classicIndex),
category = WalletCategory.CLASSIC.value,
))
}
)
}
Expand Down Expand Up @@ -195,6 +203,9 @@ fun createSignedWeb3AddressRequest(
chainId == Constants.ChainId.BITCOIN_CHAIN_ID || chainId == Constants.ChainId.PEARL_CHAIN_ID -> {
signUtxoAddressMessage(privateKey, message, chainId)
}
chainId == Constants.ChainId.TRON_CHAIN_ID -> {
one.mixin.android.crypto.TronKeyGenerator.signMessageV2(privateKey, message)
}
else -> null
}
} else {
Expand Down
Loading