mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 09:05:48 +00:00
Add Tron (TRC20/TRX) send/transfer support
Reuses the existing generic fee-sufficiency validation (sufficientTransferableBalanceToPayOriginFee) and send flow unchanged. Transaction construction goes through TronGrid's own createtransaction/triggersmartcontract endpoints (verified live against Shasta testnet) rather than a hand-rolled protobuf implementation; signing reuses the existing secp256k1 path (SignerPayloadRaw.skipMessageHashing) already used for Ethereum, no new crypto library. Also fixes a Phase 1 gap: Chain.isValidAddress() had no isTronBased branch, so every Tron send would have failed address validation. No Energy staking/delegation/rental UI - only Tron's default protocol behavior (burn TRX to cover Bandwidth/Energy shortfall).
This commit is contained in:
@@ -2,6 +2,7 @@ package io.novafoundation.nova.common.utils
|
||||
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumPublicKey
|
||||
import io.novasama.substrate_sdk_android.extensions.toAccountId
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigInteger
|
||||
|
||||
@@ -113,3 +114,21 @@ fun String.tronAddressToAccountId(): AccountId {
|
||||
fun String.isValidTronAddress(): Boolean = runCatching { tronAddressToAccountId() }.isSuccess
|
||||
|
||||
fun emptyTronAccountId() = ByteArray(20) { 1 }
|
||||
|
||||
/**
|
||||
* Hex form of a Tron address (`0x41` prefix byte ++ accountId, hex-encoded, no `0x` prefix), e.g.
|
||||
* `41a614f803b6fd780986a42c78ec9c7f77e6ded13c`. This is the format TronGrid's `/wallet/*` transaction
|
||||
* construction/broadcast endpoints expect when called with `"visible": false` (as opposed to the human-facing
|
||||
* Base58Check form used by the `/v1/accounts/{address}` balance endpoint and by [toTronAddress]).
|
||||
*/
|
||||
fun AccountId.toTronHexAddress(): String {
|
||||
require(size == 20) { "Tron account id must be 20 bytes, got $size" }
|
||||
|
||||
return byteArrayOf(TRON_ADDRESS_PREFIX_BYTE).toHexString(withPrefix = false) + toHexString(withPrefix = false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a human-facing Base58Check Tron address (e.g. a TRC20 `contractAddress` from chain config) directly
|
||||
* into the hex form described in [toTronHexAddress].
|
||||
*/
|
||||
fun String.tronAddressToHexAddress(): String = tronAddressToAccountId().toTronHexAddress()
|
||||
|
||||
@@ -44,6 +44,18 @@ class TronAddressTest {
|
||||
assertTrue(decodedBack.contentEquals(accountId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toTronHexAddress should produce the known hex form`() {
|
||||
val accountId = knownTronAddressHex.fromHex().copyOfRange(1, 21)
|
||||
|
||||
assertEquals(knownTronAddressHex, accountId.toTronHexAddress())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tronAddressToHexAddress should produce the known hex form directly from a Base58 address`() {
|
||||
assertEquals(knownTronAddressHex, knownTronAddress.tronAddressToHexAddress())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidTronAddress should accept known good address`() {
|
||||
assertTrue(knownTronAddress.isValidTronAddress())
|
||||
|
||||
+13
@@ -63,6 +63,19 @@ class SubstrateFee(
|
||||
override val asset: Chain.Asset
|
||||
) : Fee
|
||||
|
||||
/**
|
||||
* Fee for a Tron transaction (native TRX or TRC-20), always denominated in TRX (sun), regardless of which asset
|
||||
* is being sent - Tron has no separate "gas token" concept, network resources (bandwidth/energy) are always
|
||||
* burned as TRX. [amount] is this client's own estimate of that burn (see `RealTronTransactionService`); the
|
||||
* network only ever burns what it actually uses, so the real cost can be lower, but never higher than what this
|
||||
* client authorized via `fee_limit` when submitting.
|
||||
*/
|
||||
class TronFee(
|
||||
override val amount: BigInteger,
|
||||
override val submissionOrigin: SubmissionOrigin,
|
||||
override val asset: Chain.Asset
|
||||
) : Fee
|
||||
|
||||
class SubstrateFeeBase(
|
||||
override val amount: BigInteger,
|
||||
override val asset: Chain.Asset
|
||||
|
||||
+2
@@ -8,4 +8,6 @@ sealed interface TransactionExecution {
|
||||
class Ethereum(val ethereumTransactionExecution: EthereumTransactionExecution) : TransactionExecution
|
||||
|
||||
class Substrate(val extrinsicExecutionResult: ExtrinsicExecutionResult) : TransactionExecution
|
||||
|
||||
class Tron(val hash: String) : TransactionExecution
|
||||
}
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.trc20
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.TransactionExecution
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.checkForFeeChanges
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction.TronTransactionIntent
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction.TronTransactionService
|
||||
import io.novafoundation.nova.feature_wallet_impl.domain.validaiton.recipientCanAcceptTransfer
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.ext.requireTrc20
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* TRC-20 token transfer (e.g. USDT-TRC20). The fee is always denominated in native TRX, never in the TRC-20
|
||||
* token being sent - same pattern as an ERC-20 transfer's fee being paid in ETH, not the ERC-20 token (compare
|
||||
* [io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.evmErc20.EvmErc20AssetTransfers]).
|
||||
* This falls out for free from [TronTransactionService.calculateFee] always returning a [io.novafoundation.nova.feature_account_api.data.model.TronFee]
|
||||
* denominated in `chain.commissionAsset` (native TRX), combined with the fully-generic
|
||||
* [sufficientTransferableBalanceToPayOriginFee] validation checking that commission asset's balance regardless
|
||||
* of which asset is actually being transferred.
|
||||
*/
|
||||
class Trc20AssetTransfers(
|
||||
private val tronTransactionService: TronTransactionService,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : AssetTransfers {
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope) = ValidationSystem {
|
||||
validAddress()
|
||||
recipientIsNotSystemAccount()
|
||||
|
||||
positiveAmount()
|
||||
|
||||
sufficientBalanceInUsedAsset()
|
||||
sufficientTransferableBalanceToPayOriginFee()
|
||||
|
||||
recipientCanAcceptTransfer(assetSourceRegistry)
|
||||
|
||||
checkForFeeChanges(assetSourceRegistry, coroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee {
|
||||
return tronTransactionService.calculateFee(
|
||||
chain = transfer.originChain,
|
||||
origin = transfer.sender.intoOrigin(),
|
||||
recipient = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
intent = transfer.intoTrc20Intent()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission> {
|
||||
return tronTransactionService.transact(
|
||||
chain = transfer.originChain,
|
||||
origin = transfer.sender.intoOrigin(),
|
||||
recipient = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
intent = transfer.intoTrc20Intent()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun performTransferAndAwaitExecution(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<TransactionExecution> {
|
||||
return tronTransactionService.transactAndAwaitExecution(
|
||||
chain = transfer.originChain,
|
||||
origin = transfer.sender.intoOrigin(),
|
||||
recipient = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
intent = transfer.intoTrc20Intent()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun AssetTransfer.intoTrc20Intent(): TronTransactionIntent.Trc20Transfer {
|
||||
val trc20 = originChainAsset.requireTrc20()
|
||||
|
||||
return TronTransactionIntent.Trc20Transfer(trc20.contractAddress, amountInPlanks)
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.tronNative
|
||||
|
||||
import io.novafoundation.nova.common.validation.ValidationSystem
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.intoOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.TransactionExecution
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.WeightedAssetTransfer
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.amountInPlanks
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.model.TransferParsedFromCall
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.checkForFeeChanges
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.positiveAmount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.recipientIsNotSystemAccount
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientBalanceInUsedAsset
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.sufficientTransferableBalanceToPayOriginFee
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.validations.validAddress
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction.TronTransactionIntent
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction.TronTransactionService
|
||||
import io.novafoundation.nova.feature_wallet_impl.domain.validaiton.recipientCanAcceptTransfer
|
||||
import io.novafoundation.nova.runtime.ext.accountIdOrDefault
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.definitions.types.generics.GenericCall
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
/**
|
||||
* Native TRX transfer. No Energy/Bandwidth staking or rental is implemented (out of scope for this send-only
|
||||
* phase) - Tron's default protocol behavior (automatically burning TRX when free Bandwidth is insufficient) is
|
||||
* all that's needed; [TronTransactionService] estimates that burn and reports it as [Fee], and the generic
|
||||
* [sufficientTransferableBalanceToPayOriginFee] validation (same one EVM native/ERC-20 transfers already reuse)
|
||||
* blocks the send if the TRX balance can't cover it.
|
||||
*/
|
||||
class TronNativeAssetTransfers(
|
||||
private val tronTransactionService: TronTransactionService,
|
||||
private val assetSourceRegistry: AssetSourceRegistry,
|
||||
) : AssetTransfers {
|
||||
|
||||
override fun getValidationSystem(coroutineScope: CoroutineScope) = ValidationSystem {
|
||||
validAddress()
|
||||
recipientIsNotSystemAccount()
|
||||
|
||||
positiveAmount()
|
||||
|
||||
sufficientBalanceInUsedAsset()
|
||||
sufficientTransferableBalanceToPayOriginFee()
|
||||
|
||||
recipientCanAcceptTransfer(assetSourceRegistry)
|
||||
|
||||
checkForFeeChanges(assetSourceRegistry, coroutineScope)
|
||||
}
|
||||
|
||||
override suspend fun calculateFee(transfer: AssetTransfer, coroutineScope: CoroutineScope): Fee {
|
||||
return tronTransactionService.calculateFee(
|
||||
chain = transfer.originChain,
|
||||
origin = transfer.sender.intoOrigin(),
|
||||
recipient = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
intent = TronTransactionIntent.Native(transfer.amountInPlanks)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun performTransfer(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<ExtrinsicSubmission> {
|
||||
return tronTransactionService.transact(
|
||||
chain = transfer.originChain,
|
||||
origin = transfer.sender.intoOrigin(),
|
||||
recipient = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
intent = TronTransactionIntent.Native(transfer.amountInPlanks)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun performTransferAndAwaitExecution(transfer: WeightedAssetTransfer, coroutineScope: CoroutineScope): Result<TransactionExecution> {
|
||||
return tronTransactionService.transactAndAwaitExecution(
|
||||
chain = transfer.originChain,
|
||||
origin = transfer.sender.intoOrigin(),
|
||||
recipient = transfer.originChain.accountIdOrDefault(transfer.recipient),
|
||||
presetFee = transfer.fee.submissionFee,
|
||||
intent = TronTransactionIntent.Native(transfer.amountInPlanks)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun areTransfersEnabled(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun parseTransfer(call: GenericCall.Instance, chain: Chain): TransferParsedFromCall? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+35
@@ -1,9 +1,20 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron
|
||||
|
||||
import io.novafoundation.nova.common.data.network.UserAgent
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronAccountResourceResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronAccountResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronAddressRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronBroadcastRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronBroadcastResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronChainParametersResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronCreateTransactionRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronTriggerContractRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronTriggerContractResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronUnsignedTransactionResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface RetrofitTronGridApi {
|
||||
@@ -11,4 +22,28 @@ interface RetrofitTronGridApi {
|
||||
@GET
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun getAccount(@Url url: String): TronAccountResponse
|
||||
|
||||
@POST
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun createTransaction(@Url url: String, @Body body: TronCreateTransactionRequest): TronUnsignedTransactionResponse
|
||||
|
||||
@POST
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun triggerConstantContract(@Url url: String, @Body body: TronTriggerContractRequest): TronTriggerContractResponse
|
||||
|
||||
@POST
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun triggerSmartContract(@Url url: String, @Body body: TronTriggerContractRequest): TronTriggerContractResponse
|
||||
|
||||
@POST
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun broadcastTransaction(@Url url: String, @Body body: TronBroadcastRequest): TronBroadcastResponse
|
||||
|
||||
@GET
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun getChainParameters(@Url url: String): TronChainParametersResponse
|
||||
|
||||
@POST
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun getAccountResource(@Url url: String, @Body body: TronAddressRequest): TronAccountResourceResponse
|
||||
}
|
||||
|
||||
+179
@@ -1,13 +1,82 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronAccountResourceResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronAddressRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronBroadcastRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronBroadcastResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronCreateTransactionRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronTriggerContractRequest
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronTriggerContractResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronUnsignedTransactionResponse
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* Thrown whenever TronGrid reports a failure via an HTTP-200 body (rather than an HTTP error status), which is
|
||||
* how most `/wallet/*` endpoints signal validation/execution failures, e.g.
|
||||
* `{"Error": "... no OwnerAccount."}` from `createtransaction`, or
|
||||
* `{"code": "CONTRACT_VALIDATE_ERROR", "message": "<hex>"}` from `broadcasttransaction`.
|
||||
*/
|
||||
class TronApiException(message: String) : Exception(message)
|
||||
|
||||
interface TronGridApi {
|
||||
|
||||
suspend fun fetchNativeBalance(baseUrl: String, address: String): Balance
|
||||
|
||||
suspend fun fetchTrc20Balance(baseUrl: String, address: String, contractAddress: String): Balance
|
||||
|
||||
/**
|
||||
* Builds an unsigned native TRX transfer via `POST /wallet/createtransaction`.
|
||||
* [ownerHexAddress]/[toHexAddress] must be in hex form (`41`-prefixed), matching `visible: false`.
|
||||
*
|
||||
* Note: TronGrid refuses to build this for an owner account that has never been activated on-chain
|
||||
* (confirmed live: returns `{"Error": "... no OwnerAccount."}`) - unlike [triggerSmartContract], which
|
||||
* happily builds a transaction for an unfunded/unactivated owner.
|
||||
*/
|
||||
suspend fun createNativeTransfer(baseUrl: String, ownerHexAddress: String, toHexAddress: String, amountSun: BigInteger): TronUnsignedTransactionResponse
|
||||
|
||||
/**
|
||||
* Read-only dry run via `POST /wallet/triggerconstantcontract` - does not require the owner account to hold
|
||||
* any TRX and does not touch chain state. Used to estimate the `energy_used` an actual TRC-20 call would
|
||||
* cost (see [TronTriggerContractResponse.energyUsed]).
|
||||
*/
|
||||
suspend fun triggerConstantContract(
|
||||
baseUrl: String,
|
||||
ownerHexAddress: String,
|
||||
contractHexAddress: String,
|
||||
functionSelector: String,
|
||||
parameterHex: String
|
||||
): TronTriggerContractResponse
|
||||
|
||||
/**
|
||||
* Builds an unsigned TRC-20 contract call via `POST /wallet/triggersmartcontract`. Unlike
|
||||
* [createNativeTransfer], this works even for an owner account that has never been activated on-chain
|
||||
* (confirmed live).
|
||||
*/
|
||||
suspend fun triggerSmartContract(
|
||||
baseUrl: String,
|
||||
ownerHexAddress: String,
|
||||
contractHexAddress: String,
|
||||
functionSelector: String,
|
||||
parameterHex: String,
|
||||
feeLimitSun: BigInteger
|
||||
): TronTriggerContractResponse
|
||||
|
||||
/**
|
||||
* Signs-and-submits via `POST /wallet/broadcasttransaction`. The full [unsigned] transaction (including its
|
||||
* `raw_data` object, not just `raw_data_hex`) must be echoed back verbatim alongside the signature - sending
|
||||
* only `raw_data_hex` + `signature` was confirmed live to fail with a deserialization error on TronGrid's side.
|
||||
*
|
||||
* @return the transaction hash (`txID`) on success.
|
||||
* @throws TronApiException if TronGrid rejects the broadcast (invalid signature, insufficient balance, etc.)
|
||||
*/
|
||||
suspend fun broadcastTransaction(baseUrl: String, unsigned: TronUnsignedTransactionResponse, signatureHex: String): String
|
||||
|
||||
/** `key -> value` map from `GET /wallet/getchainparameters`, e.g. `getEnergyFee` (sun/energy), `getTransactionFee` (sun/byte). */
|
||||
suspend fun getChainParameters(baseUrl: String): Map<String, Long>
|
||||
|
||||
suspend fun getAccountResource(baseUrl: String, addressHex: String): TronAccountResourceResponse
|
||||
}
|
||||
|
||||
class RealTronGridApi(
|
||||
@@ -29,6 +98,93 @@ class RealTronGridApi(
|
||||
return rawBalance?.toBigIntegerOrNull() ?: BigInteger.ZERO
|
||||
}
|
||||
|
||||
override suspend fun createNativeTransfer(baseUrl: String, ownerHexAddress: String, toHexAddress: String, amountSun: BigInteger): TronUnsignedTransactionResponse {
|
||||
val request = TronCreateTransactionRequest(
|
||||
ownerAddress = ownerHexAddress,
|
||||
toAddress = toHexAddress,
|
||||
amount = amountSun.toLongExactOrThrow("amount")
|
||||
)
|
||||
|
||||
val response = retrofitApi.createTransaction(walletUrl(baseUrl, "createtransaction"), request)
|
||||
|
||||
return response.requireConstructed()
|
||||
}
|
||||
|
||||
override suspend fun triggerConstantContract(
|
||||
baseUrl: String,
|
||||
ownerHexAddress: String,
|
||||
contractHexAddress: String,
|
||||
functionSelector: String,
|
||||
parameterHex: String
|
||||
): TronTriggerContractResponse {
|
||||
val request = TronTriggerContractRequest(
|
||||
ownerAddress = ownerHexAddress,
|
||||
contractAddress = contractHexAddress,
|
||||
functionSelector = functionSelector,
|
||||
parameter = parameterHex
|
||||
)
|
||||
|
||||
return retrofitApi.triggerConstantContract(walletUrl(baseUrl, "triggerconstantcontract"), request)
|
||||
}
|
||||
|
||||
override suspend fun triggerSmartContract(
|
||||
baseUrl: String,
|
||||
ownerHexAddress: String,
|
||||
contractHexAddress: String,
|
||||
functionSelector: String,
|
||||
parameterHex: String,
|
||||
feeLimitSun: BigInteger
|
||||
): TronTriggerContractResponse {
|
||||
val request = TronTriggerContractRequest(
|
||||
ownerAddress = ownerHexAddress,
|
||||
contractAddress = contractHexAddress,
|
||||
functionSelector = functionSelector,
|
||||
parameter = parameterHex,
|
||||
feeLimit = feeLimitSun.toLongExactOrThrow("feeLimit")
|
||||
)
|
||||
|
||||
val response = retrofitApi.triggerSmartContract(walletUrl(baseUrl, "triggersmartcontract"), request)
|
||||
|
||||
if (response.result?.result != true) {
|
||||
throw TronApiException(response.result?.message ?: response.result?.code ?: "triggersmartcontract failed without a message")
|
||||
}
|
||||
|
||||
// Only validate that a transaction was actually returned - callers read [TronTriggerContractResponse.transaction] themselves.
|
||||
response.transaction?.requireConstructed()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
override suspend fun broadcastTransaction(baseUrl: String, unsigned: TronUnsignedTransactionResponse, signatureHex: String): String {
|
||||
val txId = requireNotNull(unsigned.txID) { "Cannot broadcast a transaction without a txID" }
|
||||
|
||||
val request = TronBroadcastRequest(
|
||||
visible = unsigned.visible ?: false,
|
||||
txID = txId,
|
||||
rawData = requireNotNull(unsigned.rawData) { "Cannot broadcast a transaction without raw_data" },
|
||||
rawDataHex = requireNotNull(unsigned.rawDataHex) { "Cannot broadcast a transaction without raw_data_hex" },
|
||||
signature = listOf(signatureHex)
|
||||
)
|
||||
|
||||
val response = retrofitApi.broadcastTransaction(walletUrl(baseUrl, "broadcasttransaction"), request)
|
||||
|
||||
if (response.result != true) {
|
||||
throw TronApiException(response.decodeErrorMessage())
|
||||
}
|
||||
|
||||
return response.txid ?: txId
|
||||
}
|
||||
|
||||
override suspend fun getChainParameters(baseUrl: String): Map<String, Long> {
|
||||
return retrofitApi.getChainParameters(walletUrl(baseUrl, "getchainparameters"))
|
||||
.chainParameter
|
||||
.associate { it.key to it.value }
|
||||
}
|
||||
|
||||
override suspend fun getAccountResource(baseUrl: String, addressHex: String): TronAccountResourceResponse {
|
||||
return retrofitApi.getAccountResource(walletUrl(baseUrl, "getaccountresource"), TronAddressRequest(address = addressHex))
|
||||
}
|
||||
|
||||
private suspend fun fetchAccountData(baseUrl: String, address: String) = retrofitApi.getAccount(
|
||||
url = accountUrl(baseUrl, address)
|
||||
).data?.firstOrNull()
|
||||
@@ -36,4 +192,27 @@ class RealTronGridApi(
|
||||
private fun accountUrl(baseUrl: String, address: String): String {
|
||||
return "${baseUrl.trimEnd('/')}/v1/accounts/$address"
|
||||
}
|
||||
|
||||
private fun walletUrl(baseUrl: String, method: String): String {
|
||||
return "${baseUrl.trimEnd('/')}/wallet/$method"
|
||||
}
|
||||
|
||||
private fun TronUnsignedTransactionResponse.requireConstructed(): TronUnsignedTransactionResponse {
|
||||
if (error != null) throw TronApiException(error)
|
||||
requireNotNull(rawDataHex) { "TronGrid returned no raw_data_hex and no Error" }
|
||||
requireNotNull(txID) { "TronGrid returned no txID and no Error" }
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun TronBroadcastResponse.decodeErrorMessage(): String {
|
||||
val decodedMessage = message?.let { hex -> runCatching { hex.fromHex().decodeToString() }.getOrNull() }
|
||||
|
||||
return decodedMessage ?: code ?: "broadcasttransaction failed without a message"
|
||||
}
|
||||
|
||||
private fun BigInteger.toLongExactOrThrow(fieldName: String): Long {
|
||||
return runCatching { longValueExact() }
|
||||
.getOrElse { throw IllegalArgumentException("$fieldName overflows Long: $this") }
|
||||
}
|
||||
}
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron.model
|
||||
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Request/response shapes for TronGrid's transaction-construction/broadcast endpoints (`/wallet/*`).
|
||||
*
|
||||
* All requests are sent with `"visible": false`, i.e. addresses are hex-encoded (`41` prefix byte ++ 20-byte
|
||||
* accountId, see `toTronHexAddress`) rather than Base58Check. Every shape below was confirmed against
|
||||
* TronGrid's Shasta testnet (`https://api.shasta.trongrid.io`) with live HTTP calls - see the Phase 2
|
||||
* implementation notes for the exact request/response pairs that were captured.
|
||||
*/
|
||||
|
||||
class TronCreateTransactionRequest(
|
||||
@SerializedName("owner_address") val ownerAddress: String,
|
||||
@SerializedName("to_address") val toAddress: String,
|
||||
val amount: Long,
|
||||
val visible: Boolean = false,
|
||||
)
|
||||
|
||||
class TronTriggerContractRequest(
|
||||
@SerializedName("owner_address") val ownerAddress: String,
|
||||
@SerializedName("contract_address") val contractAddress: String,
|
||||
@SerializedName("function_selector") val functionSelector: String,
|
||||
val parameter: String,
|
||||
@SerializedName("fee_limit") val feeLimit: Long? = null,
|
||||
@SerializedName("call_value") val callValue: Long = 0,
|
||||
val visible: Boolean = false,
|
||||
)
|
||||
|
||||
class TronAddressRequest(
|
||||
val address: String,
|
||||
val visible: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* Shape of the unsigned transaction returned by both `/wallet/createtransaction` (flat, at the top level) and
|
||||
* `/wallet/triggersmartcontract`/`/wallet/triggerconstantcontract` (nested under a `transaction` key - see
|
||||
* [TronTriggerContractResponse]).
|
||||
*
|
||||
* `rawData` is kept as an opaque [JsonObject] rather than being modeled field-by-field: its contents differ
|
||||
* between contract types (`TransferContract` vs `TriggerSmartContract`) and it is never interpreted by this
|
||||
* client - it is only ever echoed back verbatim into the broadcast request alongside the signature. The
|
||||
* cryptographically-authoritative value is [rawDataHex] (`txID == sha256(rawDataHex bytes)`, confirmed live).
|
||||
*
|
||||
* `error` is populated (HTTP 200, not an HTTP error) when construction fails, e.g. an unactivated owner account
|
||||
* trying to build a native TRX transfer returns `{"Error": "... no OwnerAccount."}`.
|
||||
*/
|
||||
class TronUnsignedTransactionResponse(
|
||||
val visible: Boolean? = null,
|
||||
val txID: String? = null,
|
||||
@SerializedName("raw_data") val rawData: JsonObject? = null,
|
||||
@SerializedName("raw_data_hex") val rawDataHex: String? = null,
|
||||
@SerializedName("Error") val error: String? = null,
|
||||
)
|
||||
|
||||
class TronContractCallResult(
|
||||
val result: Boolean = false,
|
||||
val code: String? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* Response of both `/wallet/triggerconstantcontract` (read-only dry run, used for TRC20 fee/energy estimation)
|
||||
* and `/wallet/triggersmartcontract` (real construction, used for the actual TRC20 transfer).
|
||||
*/
|
||||
class TronTriggerContractResponse(
|
||||
val result: TronContractCallResult? = null,
|
||||
@SerializedName("energy_used") val energyUsed: Long? = null,
|
||||
@SerializedName("constant_result") val constantResult: List<String>? = null,
|
||||
val transaction: TronUnsignedTransactionResponse? = null,
|
||||
)
|
||||
|
||||
class TronBroadcastRequest(
|
||||
val visible: Boolean,
|
||||
val txID: String,
|
||||
@SerializedName("raw_data") val rawData: JsonObject,
|
||||
@SerializedName("raw_data_hex") val rawDataHex: String,
|
||||
val signature: List<String>,
|
||||
)
|
||||
|
||||
/**
|
||||
* On success: `{"result": true, "txid": "..."}`.
|
||||
* On failure: `{"code": "CONTRACT_VALIDATE_ERROR", "txid": "...", "message": "<hex-encoded ascii>"}` - confirmed
|
||||
* live by broadcasting a validly-signed but unfunded-account transaction, e.g.
|
||||
* `message` hex-decodes to `"Contract validate error : account [...] does not exist"`.
|
||||
*/
|
||||
class TronBroadcastResponse(
|
||||
val result: Boolean? = null,
|
||||
val txid: String? = null,
|
||||
val code: String? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
class TronChainParameter(
|
||||
val key: String,
|
||||
val value: Long = 0,
|
||||
)
|
||||
|
||||
class TronChainParametersResponse(
|
||||
@SerializedName("chainParameter") val chainParameter: List<TronChainParameter> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Subset of `/wallet/getaccountresource` fields relevant to fee estimation. Fields are omitted by TronGrid
|
||||
* (rather than sent as `0`) when their value is zero - confirmed live - hence all default to `0`.
|
||||
*/
|
||||
class TronAccountResourceResponse(
|
||||
val freeNetLimit: Long = 0,
|
||||
val freeNetUsed: Long = 0,
|
||||
@SerializedName("NetLimit") val netLimit: Long = 0,
|
||||
@SerializedName("NetUsed") val netUsed: Long = 0,
|
||||
@SerializedName("EnergyLimit") val energyLimit: Long = 0,
|
||||
@SerializedName("EnergyUsed") val energyUsed: Long = 0,
|
||||
)
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction
|
||||
|
||||
import io.novafoundation.nova.common.utils.castOrNull
|
||||
import io.novafoundation.nova.common.utils.sha256
|
||||
import io.novafoundation.nova.common.utils.toEcdsaSignatureData
|
||||
import io.novafoundation.nova.common.utils.toTronHexAddress
|
||||
import io.novafoundation.nova.common.utils.tronAddressToHexAddress
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.SubmissionOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_account_api.data.model.TronFee
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.CallExecutionType
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.SubmissionHierarchy
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.requireMetaAccountFor
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.requireAccountIdIn
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.TransactionExecution
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.TronGridApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronAccountResourceResponse
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.model.TronUnsignedTransactionResponse
|
||||
import io.novafoundation.nova.runtime.ext.commissionAsset
|
||||
import io.novafoundation.nova.runtime.ext.requireTronGridBaseUrl
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import io.novasama.substrate_sdk_android.runtime.extrinsic.signer.SignerPayloadRaw
|
||||
import java.math.BigInteger
|
||||
|
||||
private const val ENERGY_FEE_PARAM_KEY = "getEnergyFee"
|
||||
private const val TRANSACTION_FEE_PARAM_KEY = "getTransactionFee"
|
||||
|
||||
// Fallbacks only used if a live `getchainparameters` call fails or is missing the expected key - both values are
|
||||
// what was observed live on Shasta testnet at implementation time, which also match Tron's long-standing mainnet
|
||||
// defaults; the primary path always fetches live values.
|
||||
private val FALLBACK_ENERGY_FEE_SUN = BigInteger.valueOf(420)
|
||||
private val FALLBACK_BANDWIDTH_FEE_SUN = BigInteger.valueOf(1000)
|
||||
|
||||
// Used only if a triggerconstantcontract dry run fails outright (e.g. transient network error) and returns no
|
||||
// energy_used at all - a conservative (intentionally high) stand-in for a simple TRC-20 transfer, which in
|
||||
// practice costs on the order of 15-30k energy. Mirrors EvmErc20AssetTransfers' ERC_20_UPPER_GAS_LIMIT fallback.
|
||||
private val FALLBACK_TRC20_ENERGY_UNITS = 65_000L
|
||||
private val FALLBACK_TRC20_TX_SIZE_BYTES = 350L
|
||||
|
||||
// fee_limit sent with triggersmartcontract: Tron only ever burns what a call actually uses (up to this cap), so
|
||||
// setting this generously above our own estimate does not cost the user more - it only avoids an OUT_OF_ENERGY
|
||||
// failure if our estimate undershoots. Bounded above as a sanity guard against a runaway estimate.
|
||||
private val MIN_FEE_LIMIT_SUN = BigInteger.valueOf(15_000_000) // 15 TRX
|
||||
private val MAX_FEE_LIMIT_SUN = BigInteger.valueOf(100_000_000) // 100 TRX
|
||||
|
||||
private val EMPTY_RESOURCE = TronAccountResourceResponse()
|
||||
|
||||
/**
|
||||
* Builds, signs and broadcasts Tron transactions (native TRX and TRC-20) using TronGrid's own REST endpoints for
|
||||
* construction/broadcast, and this app's existing ECDSA signing primitive for signing - no Tron protobuf
|
||||
* (`Transaction`/`TransferContract`/`TriggerSmartContract`) encoding and no new crypto library were added.
|
||||
*
|
||||
* ## Construction
|
||||
* - Native TRX: `POST /wallet/createtransaction` with `{owner_address, to_address, amount}` (all hex-encoded,
|
||||
* `visible: false`). Requires the owner account to already be activated on-chain (confirmed live: an
|
||||
* unactivated owner gets `{"Error": "... no OwnerAccount."}`) - in practice this is never hit here, since a
|
||||
* user can only reach the send flow with a positive TRX balance to send from, which itself implies the account
|
||||
* was already activated by an earlier incoming transfer.
|
||||
* - TRC-20: `POST /wallet/triggersmartcontract` with the ABI-encoded `transfer(address,uint256)` call (see
|
||||
* [Trc20TransferAbi]). Unlike `createtransaction`, this was confirmed live to work even for a
|
||||
* never-activated owner account.
|
||||
*
|
||||
* ## Signing
|
||||
* Tron's signature is `ECDSA_sign(privateKey, sha256(raw_data))` over secp256k1 - the same curve/primitive this
|
||||
* app already uses for Ethereum. [io.novafoundation.nova.feature_account_api.data.signer.NovaSigner.signRaw]
|
||||
* (backed by `substrate_sdk_android`'s `Signer.sign(MultiChainEncryption.Ethereum, message, keypair, skipHashing)`
|
||||
* -> `web3j`'s `Sign.signMessage(hash, keyPair, needToHash = false)`) already supports signing a pre-computed
|
||||
* hash directly via `SignerPayloadRaw.skipMessageHashing = true` - this is exactly the primitive Ethereum-style
|
||||
* raw-hash signing needs, and it is reused as-is here. No new cryptographic code or library was added; only the
|
||||
* hash fed into it differs from the EVM path (`sha256(raw_data)` here vs. an EIP-155 RLP-based digest there).
|
||||
*
|
||||
* `web3j`'s `Sign.signMessage` always left-pads `r`/`s` to exactly 32 bytes and encodes `v` as `27/28` (confirmed
|
||||
* by reading `web3j`'s `Sign.java` source) - which is byte-for-byte the same `r(32) + s(32) + v(1)` = 65-byte
|
||||
* compact signature format Tron expects (confirmed against `tronweb`'s own `ECKeySign` implementation, and
|
||||
* independently confirmed live against Shasta testnet - see the Phase 2 implementation notes).
|
||||
*
|
||||
* ## Broadcast
|
||||
* `POST /wallet/broadcasttransaction` with the full unsigned transaction object (not just `raw_data_hex`) plus
|
||||
* `signature: [<65-byte hex signature>]`.
|
||||
*/
|
||||
class RealTronTransactionService(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val signerProvider: SignerProvider,
|
||||
private val tronGridApi: TronGridApi,
|
||||
) : TronTransactionService {
|
||||
|
||||
override suspend fun calculateFee(chain: Chain, origin: TransactionOrigin, recipient: AccountId, intent: TronTransactionIntent): Fee {
|
||||
val submittingMetaAccount = accountRepository.requireMetaAccountFor(origin, chain.id)
|
||||
val ownerAccountId = submittingMetaAccount.requireAccountIdIn(chain)
|
||||
val baseUrl = chain.requireTronGridBaseUrl()
|
||||
val ownerHex = ownerAccountId.toTronHexAddress()
|
||||
|
||||
val feeSun = when (intent) {
|
||||
is TronTransactionIntent.Native -> estimateNativeFee(baseUrl, ownerHex, recipient, intent.amountSun)
|
||||
is TronTransactionIntent.Trc20Transfer -> estimateTrc20FeeFromContractHex(baseUrl, ownerHex, recipient, intent.contractAddress.tronAddressToHexAddress(), intent.amountSun)
|
||||
}
|
||||
|
||||
return TronFee(feeSun, SubmissionOrigin.singleOrigin(ownerAccountId), chain.commissionAsset)
|
||||
}
|
||||
|
||||
override suspend fun transact(
|
||||
chain: Chain,
|
||||
origin: TransactionOrigin,
|
||||
recipient: AccountId,
|
||||
presetFee: Fee?,
|
||||
intent: TronTransactionIntent
|
||||
): Result<ExtrinsicSubmission> = runCatching {
|
||||
val submittingMetaAccount = accountRepository.requireMetaAccountFor(origin, chain.id)
|
||||
val ownerAccountId = submittingMetaAccount.requireAccountIdIn(chain)
|
||||
val baseUrl = chain.requireTronGridBaseUrl()
|
||||
val ownerHex = ownerAccountId.toTronHexAddress()
|
||||
val recipientHex = recipient.toTronHexAddress()
|
||||
|
||||
val unsigned = when (intent) {
|
||||
is TronTransactionIntent.Native -> tronGridApi.createNativeTransfer(baseUrl, ownerHex, recipientHex, intent.amountSun)
|
||||
|
||||
is TronTransactionIntent.Trc20Transfer -> {
|
||||
val contractHex = intent.contractAddress.tronAddressToHexAddress()
|
||||
val parameterHex = Trc20TransferAbi.encodeTransferParameters(recipient, intent.amountSun)
|
||||
val feeLimit = feeLimitFor(presetFee, baseUrl, ownerHex, recipient, contractHex, intent.amountSun)
|
||||
|
||||
tronGridApi.triggerSmartContract(
|
||||
baseUrl = baseUrl,
|
||||
ownerHexAddress = ownerHex,
|
||||
contractHexAddress = contractHex,
|
||||
functionSelector = Trc20TransferAbi.TRANSFER_FUNCTION_SELECTOR,
|
||||
parameterHex = parameterHex,
|
||||
feeLimitSun = feeLimit
|
||||
).transaction ?: error("TronGrid returned no transaction from triggersmartcontract")
|
||||
}
|
||||
}
|
||||
|
||||
val txHash = signAndBroadcast(baseUrl, unsigned, submittingMetaAccount, ownerAccountId)
|
||||
|
||||
ExtrinsicSubmission(
|
||||
hash = txHash,
|
||||
submissionOrigin = SubmissionOrigin.singleOrigin(ownerAccountId),
|
||||
callExecutionType = CallExecutionType.IMMEDIATE,
|
||||
submissionHierarchy = SubmissionHierarchy(submittingMetaAccount, CallExecutionType.IMMEDIATE)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun transactAndAwaitExecution(
|
||||
chain: Chain,
|
||||
origin: TransactionOrigin,
|
||||
recipient: AccountId,
|
||||
presetFee: Fee?,
|
||||
intent: TronTransactionIntent
|
||||
): Result<TransactionExecution> {
|
||||
// Tron transactions execute atomically with inclusion (no separate "prepare" step, same as EVM) - so
|
||||
// successful broadcast is already a strong signal. We do not poll for block confirmation here since
|
||||
// this method sits outside the primary send flow's critical path (`SendInteractor`/`RealSendUseCase`
|
||||
// only ever call `transact`, not this) - see Phase 2 implementation notes for what remains unverified.
|
||||
return transact(chain, origin, recipient, presetFee, intent).map { TransactionExecution.Tron(it.hash) }
|
||||
}
|
||||
|
||||
private suspend fun signAndBroadcast(
|
||||
baseUrl: String,
|
||||
unsigned: TronUnsignedTransactionResponse,
|
||||
metaAccount: MetaAccount,
|
||||
ownerAccountId: AccountId
|
||||
): String {
|
||||
val rawDataHex = requireNotNull(unsigned.rawDataHex) { "TronGrid returned no raw_data_hex" }
|
||||
val messageHash = rawDataHex.fromHex().sha256()
|
||||
|
||||
check(unsigned.txID == null || messageHash.toHexString(withPrefix = false) == unsigned.txID) {
|
||||
"sha256(raw_data) does not match the txID TronGrid reported - refusing to sign a possibly-tampered transaction"
|
||||
}
|
||||
|
||||
val signer = signerProvider.rootSignerFor(metaAccount)
|
||||
val signedRaw = signer.signRaw(SignerPayloadRaw(message = messageHash, accountId = ownerAccountId, skipMessageHashing = true))
|
||||
val signature = signedRaw.toEcdsaSignatureData()
|
||||
|
||||
// Tron's compact signature format is r(32) + s(32) + v(1), v = 27/28 - byte-for-byte identical to what
|
||||
// web3j's Sign.SignatureData already produces for Ethereum signing (see class doc for verification notes).
|
||||
val signatureBytes = signature.r + signature.s + signature.v
|
||||
val signatureHex = signatureBytes.toHexString(withPrefix = false)
|
||||
|
||||
return tronGridApi.broadcastTransaction(baseUrl, unsigned, signatureHex)
|
||||
}
|
||||
|
||||
private suspend fun feeLimitFor(
|
||||
presetFee: Fee?,
|
||||
baseUrl: String,
|
||||
ownerHex: String,
|
||||
recipient: AccountId,
|
||||
contractHex: String,
|
||||
amountSun: BigInteger
|
||||
): BigInteger {
|
||||
val estimatedFee = presetFee?.castOrNull<TronFee>()?.amount
|
||||
?: estimateTrc20FeeFromContractHex(baseUrl, ownerHex, recipient, contractHex, amountSun)
|
||||
|
||||
return (estimatedFee * BigInteger.valueOf(3)).coerceIn(MIN_FEE_LIMIT_SUN, MAX_FEE_LIMIT_SUN)
|
||||
}
|
||||
|
||||
private suspend fun estimateNativeFee(baseUrl: String, ownerHex: String, recipient: AccountId, amountSun: BigInteger): BigInteger {
|
||||
val unsigned = tronGridApi.createNativeTransfer(baseUrl, ownerHex, recipient.toTronHexAddress(), amountSun)
|
||||
val txSizeBytes = requireNotNull(unsigned.rawDataHex) { "TronGrid returned no raw_data_hex" }.length / 2
|
||||
|
||||
val resource = runCatching { tronGridApi.getAccountResource(baseUrl, ownerHex) }.getOrDefault(EMPTY_RESOURCE)
|
||||
val bandwidthPrice = chainParameterOrDefault(baseUrl, TRANSACTION_FEE_PARAM_KEY, FALLBACK_BANDWIDTH_FEE_SUN)
|
||||
|
||||
val bandwidthShortfall = shortfall(txSizeBytes.toLong(), resource.availableBandwidth())
|
||||
|
||||
return bandwidthShortfall.toBigInteger() * bandwidthPrice
|
||||
}
|
||||
|
||||
private suspend fun estimateTrc20FeeFromContractHex(baseUrl: String, ownerHex: String, recipient: AccountId, contractHex: String, amountSun: BigInteger): BigInteger {
|
||||
val parameterHex = Trc20TransferAbi.encodeTransferParameters(recipient, amountSun)
|
||||
|
||||
val dryRun = runCatching {
|
||||
tronGridApi.triggerConstantContract(baseUrl, ownerHex, contractHex, Trc20TransferAbi.TRANSFER_FUNCTION_SELECTOR, parameterHex)
|
||||
}.getOrNull()
|
||||
|
||||
// A dry-run revert (e.g. the sender doesn't yet hold the token) still reports the energy spent up to the
|
||||
// revert point, which remains a meaningful (if slightly different) estimate - it is used as-is rather
|
||||
// than special-cased, only a wholly-failed HTTP call falls back to the conservative constant.
|
||||
val energyUsed = dryRun?.energyUsed ?: FALLBACK_TRC20_ENERGY_UNITS
|
||||
val txSizeBytes = dryRun?.transaction?.rawDataHex?.let { it.length / 2L } ?: FALLBACK_TRC20_TX_SIZE_BYTES
|
||||
|
||||
val resource = runCatching { tronGridApi.getAccountResource(baseUrl, ownerHex) }.getOrDefault(EMPTY_RESOURCE)
|
||||
val bandwidthPrice = chainParameterOrDefault(baseUrl, TRANSACTION_FEE_PARAM_KEY, FALLBACK_BANDWIDTH_FEE_SUN)
|
||||
val energyPrice = chainParameterOrDefault(baseUrl, ENERGY_FEE_PARAM_KEY, FALLBACK_ENERGY_FEE_SUN)
|
||||
|
||||
val bandwidthShortfall = shortfall(txSizeBytes, resource.availableBandwidth())
|
||||
val energyShortfall = shortfall(energyUsed, resource.availableEnergy())
|
||||
|
||||
return bandwidthShortfall.toBigInteger() * bandwidthPrice + energyShortfall.toBigInteger() * energyPrice
|
||||
}
|
||||
|
||||
private suspend fun chainParameterOrDefault(baseUrl: String, key: String, default: BigInteger): BigInteger {
|
||||
val params = runCatching { tronGridApi.getChainParameters(baseUrl) }.getOrNull()
|
||||
|
||||
return params?.get(key)?.toBigInteger() ?: default
|
||||
}
|
||||
|
||||
private fun shortfall(needed: Long, available: Long): Long = (needed - available.coerceAtLeast(0)).coerceAtLeast(0)
|
||||
|
||||
private fun TronAccountResourceResponse.availableBandwidth(): Long =
|
||||
(freeNetLimit - freeNetUsed).coerceAtLeast(0) + (netLimit - netUsed).coerceAtLeast(0)
|
||||
|
||||
private fun TronAccountResourceResponse.availableEnergy(): Long =
|
||||
(energyLimit - energyUsed).coerceAtLeast(0)
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction
|
||||
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* Minimal, hand-written Solidity ABI encoding for the single call this client ever makes to a TRC-20 contract:
|
||||
* `transfer(address,uint256)`.
|
||||
*
|
||||
* There is no pre-existing ABI-encoding utility reused here: Phase 1's TRC-20 balance reads
|
||||
* (`Trc20AssetBalance`) go through TronGrid's `/v1/accounts` REST endpoint, not an on-chain `balanceOf` call -
|
||||
* so no prior ABI-encoding code exists in this codebase.
|
||||
*
|
||||
* Both parameter types involved (`address`, `uint256`) are static (fixed-size), so encoding is just "left-pad
|
||||
* each to 32 bytes and concatenate" - no dynamic-type/offset table is needed. The 4-byte function selector is
|
||||
* intentionally NOT computed client-side: TronGrid accepts the human-readable `function_selector` string
|
||||
* directly and hashes it server-side (confirmed live against Shasta testnet - a call with
|
||||
* `function_selector: "transfer(address,uint256)"` and no client-computed selector correctly resolved to the
|
||||
* standard `a9059cbb` selector in the resulting `raw_data`), which avoids needing a keccak256 implementation here.
|
||||
*/
|
||||
object Trc20TransferAbi {
|
||||
|
||||
const val TRANSFER_FUNCTION_SELECTOR = "transfer(address,uint256)"
|
||||
|
||||
/**
|
||||
* @param recipient raw 20-byte Ethereum/Tron-style account id (NOT the `41`-prefixed Tron hex address -
|
||||
* ABI-encoded Solidity `address` parameters use the bare 20-byte form, confirmed live).
|
||||
*/
|
||||
fun encodeTransferParameters(recipient: AccountId, amountSun: BigInteger): String {
|
||||
require(recipient.size == 20) { "Tron/EVM-style account id must be 20 bytes, got ${recipient.size}" }
|
||||
require(amountSun.signum() >= 0) { "Amount must not be negative, got $amountSun" }
|
||||
|
||||
val addressParam = recipient.toHexString(withPrefix = false).padStart(64, '0')
|
||||
val amountParam = amountSun.toString(16).padStart(64, '0')
|
||||
|
||||
return addressParam + amountParam
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.data.ethereum.transaction.TransactionOrigin
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicSubmission
|
||||
import io.novafoundation.nova.feature_account_api.data.model.Fee
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.tranfers.TransactionExecution
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* What kind of Tron transaction to build. Both cases ultimately burn TRX for bandwidth/energy per Tron's default
|
||||
* protocol behavior - this service never stakes/rents Energy or Bandwidth, it only estimates the automatic burn
|
||||
* and lets the caller (asset transfer validation) block the send if the user's TRX balance can't cover it.
|
||||
*/
|
||||
sealed class TronTransactionIntent {
|
||||
|
||||
class Native(val amountSun: BigInteger) : TronTransactionIntent()
|
||||
|
||||
/** @param contractAddress Base58Check TRC-20 contract address, as stored in chain config (`Type.Trc20.contractAddress`). */
|
||||
class Trc20Transfer(val contractAddress: String, val amountSun: BigInteger) : TronTransactionIntent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors [io.novafoundation.nova.feature_account_api.data.ethereum.transaction.EvmTransactionService]'s shape
|
||||
* (calculateFee/transact/transactAndAwaitExecution over a sending origin), but for Tron. Unlike the EVM service,
|
||||
* this lives entirely in `feature-wallet-impl` rather than being split across `feature-account-api`/`-impl`,
|
||||
* since (for now, Phase 2 send-only scope) it is only ever consumed by `TronNativeAssetTransfers`/
|
||||
* `Trc20AssetTransfers` in this module, and it needs [io.novafoundation.nova.feature_wallet_impl.data.network.tron.TronGridApi],
|
||||
* which itself lives in this module (feature-account-impl cannot depend on feature-wallet-impl).
|
||||
*
|
||||
* Construction goes through TronGrid's own `/wallet/createtransaction` and `/wallet/triggersmartcontract`
|
||||
* endpoints rather than hand-rolled protobuf encoding - see `RealTronTransactionService` for details and the
|
||||
* live-testnet verification notes.
|
||||
*/
|
||||
interface TronTransactionService {
|
||||
|
||||
suspend fun calculateFee(chain: Chain, origin: TransactionOrigin, recipient: AccountId, intent: TronTransactionIntent): Fee
|
||||
|
||||
suspend fun transact(
|
||||
chain: Chain,
|
||||
origin: TransactionOrigin,
|
||||
recipient: AccountId,
|
||||
presetFee: Fee?,
|
||||
intent: TronTransactionIntent
|
||||
): Result<ExtrinsicSubmission>
|
||||
|
||||
suspend fun transactAndAwaitExecution(
|
||||
chain: Chain,
|
||||
origin: TransactionOrigin,
|
||||
recipient: AccountId,
|
||||
presetFee: Fee?,
|
||||
intent: TronTransactionIntent
|
||||
): Result<TransactionExecution>
|
||||
}
|
||||
+3
@@ -37,6 +37,7 @@ import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicServic
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.FeePaymentProviderRegistry
|
||||
import io.novafoundation.nova.feature_account_api.data.fee.capability.CustomFeeCapabilityFacade
|
||||
import io.novafoundation.nova.feature_account_api.data.multisig.repository.MultisigValidationsRepository
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.domain.updaters.AccountUpdateScope
|
||||
@@ -74,6 +75,8 @@ interface WalletFeatureDependencies {
|
||||
|
||||
val evmTransactionService: EvmTransactionService
|
||||
|
||||
val signerProvider: SignerProvider
|
||||
|
||||
val chainAssetDao: ChainAssetDao
|
||||
|
||||
val storageStorageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory
|
||||
|
||||
+47
-9
@@ -4,16 +4,22 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_account_api.data.signer.SignerProvider
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSource
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSourceRegistry
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.StaticAssetSource
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.trc20.Trc20AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.tronNative.TronNativeAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.history.UnsupportedAssetHistory
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.UnsupportedAssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.trc20.Trc20AssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.transfers.tronNative.TronNativeAssetTransfers
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.RealTronGridApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.RetrofitTronGridApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.TronGridApi
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction.RealTronTransactionService
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction.TronTransactionService
|
||||
import javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
@@ -23,11 +29,17 @@ annotation class TronNativeAssets
|
||||
annotation class Trc20Assets
|
||||
|
||||
/**
|
||||
* Tron/TRC-20 support - Phase 1, read-only.
|
||||
* Tron/TRC-20 support.
|
||||
*
|
||||
* Only `balance` is implemented for real; `transfers`/`history` reuse the same `Unsupported*` stubs the rest of
|
||||
* the app uses for asset types with no send/history support yet (see `UnsupportedAssetsModule`). This is
|
||||
* intentional: no transaction-building/signing code exists for Tron yet - that is future, separate work.
|
||||
* Phase 1 (read-only): `balance`.
|
||||
* Phase 2 (send/transfer, this module): `transfers`, via [TronTransactionService] - construction/broadcast
|
||||
* through TronGrid's own REST endpoints, signing through the app's existing Ethereum-style ECDSA signer (see
|
||||
* `RealTronTransactionService` for the full verification notes). No Energy/Bandwidth staking or rental: only
|
||||
* Tron's default protocol behavior (auto-burning TRX when free resources are insufficient) is estimated and
|
||||
* enforced.
|
||||
*
|
||||
* `history` remains unsupported (out of scope for this phase, same as the rest of the app's `Unsupported*`
|
||||
* stubs used for asset types without history support).
|
||||
*/
|
||||
@Module
|
||||
class TronAssetsModule {
|
||||
@@ -42,6 +54,18 @@ class TronAssetsModule {
|
||||
@FeatureScope
|
||||
fun provideTronGridApi(retrofitTronGridApi: RetrofitTronGridApi): TronGridApi = RealTronGridApi(retrofitTronGridApi)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTronTransactionService(
|
||||
accountRepository: AccountRepository,
|
||||
signerProvider: SignerProvider,
|
||||
tronGridApi: TronGridApi,
|
||||
): TronTransactionService = RealTronTransactionService(
|
||||
accountRepository = accountRepository,
|
||||
signerProvider = signerProvider,
|
||||
tronGridApi = tronGridApi
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTronNativeBalance(assetCache: AssetCache, tronGridApi: TronGridApi) = TronNativeAssetBalance(assetCache, tronGridApi)
|
||||
@@ -50,15 +74,29 @@ class TronAssetsModule {
|
||||
@FeatureScope
|
||||
fun provideTrc20Balance(assetCache: AssetCache, tronGridApi: TronGridApi) = Trc20AssetBalance(assetCache, tronGridApi)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTronNativeAssetTransfers(
|
||||
tronTransactionService: TronTransactionService,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
) = TronNativeAssetTransfers(tronTransactionService, assetSourceRegistry)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTrc20AssetTransfers(
|
||||
tronTransactionService: TronTransactionService,
|
||||
assetSourceRegistry: AssetSourceRegistry,
|
||||
) = Trc20AssetTransfers(tronTransactionService, assetSourceRegistry)
|
||||
|
||||
@Provides
|
||||
@TronNativeAssets
|
||||
@FeatureScope
|
||||
fun provideTronNativeAssetSource(
|
||||
tronNativeAssetBalance: TronNativeAssetBalance,
|
||||
unsupportedAssetTransfers: UnsupportedAssetTransfers,
|
||||
tronNativeAssetTransfers: TronNativeAssetTransfers,
|
||||
unsupportedAssetHistory: UnsupportedAssetHistory,
|
||||
): AssetSource = StaticAssetSource(
|
||||
transfers = unsupportedAssetTransfers,
|
||||
transfers = tronNativeAssetTransfers,
|
||||
balance = tronNativeAssetBalance,
|
||||
history = unsupportedAssetHistory
|
||||
)
|
||||
@@ -68,10 +106,10 @@ class TronAssetsModule {
|
||||
@FeatureScope
|
||||
fun provideTrc20AssetSource(
|
||||
trc20AssetBalance: Trc20AssetBalance,
|
||||
unsupportedAssetTransfers: UnsupportedAssetTransfers,
|
||||
trc20AssetTransfers: Trc20AssetTransfers,
|
||||
unsupportedAssetHistory: UnsupportedAssetHistory,
|
||||
): AssetSource = StaticAssetSource(
|
||||
transfers = unsupportedAssetTransfers,
|
||||
transfers = trc20AssetTransfers,
|
||||
balance = trc20AssetBalance,
|
||||
history = unsupportedAssetHistory
|
||||
)
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron.transaction
|
||||
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.math.BigInteger
|
||||
|
||||
class Trc20TransferAbiTest {
|
||||
|
||||
/**
|
||||
* Real request/response pair captured live against TronGrid's Shasta testnet
|
||||
* (`POST https://api.shasta.trongrid.io/wallet/triggerconstantcontract`) for a `transfer(address,uint256)`
|
||||
* call with recipient accountId `dfd8703a5c753e17ed52a96a29cea9d425538dfe` and amount `1000000` (sun) -
|
||||
* TronGrid accepted this exact `parameter` value and correctly resolved `function_selector` to the standard
|
||||
* `a9059cbb` selector in the resulting `raw_data.contract[0].parameter.value.data`.
|
||||
*/
|
||||
@Test
|
||||
fun `encodeTransferParameters should match a live-verified TronGrid request`() {
|
||||
val recipient = "dfd8703a5c753e17ed52a96a29cea9d425538dfe".fromHex()
|
||||
val amountSun = BigInteger.valueOf(1_000_000)
|
||||
|
||||
val expectedParameter = "000000000000000000000000dfd8703a5c753e17ed52a96a29cea9d425538dfe" +
|
||||
"00000000000000000000000000000000000000000000000000000000000f4240"
|
||||
|
||||
assertEquals(expectedParameter, Trc20TransferAbi.encodeTransferParameters(recipient, amountSun))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encodeTransferParameters should reject a negative amount`() {
|
||||
val recipient = "dfd8703a5c753e17ed52a96a29cea9d425538dfe".fromHex()
|
||||
|
||||
assertThrowsIllegalArgument {
|
||||
Trc20TransferAbi.encodeTransferParameters(recipient, BigInteger.valueOf(-1))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `encodeTransferParameters should reject a non-20-byte account id`() {
|
||||
assertThrowsIllegalArgument {
|
||||
Trc20TransferAbi.encodeTransferParameters(ByteArray(19), BigInteger.ONE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertThrowsIllegalArgument(block: () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
throw AssertionError("Expected IllegalArgumentException")
|
||||
} catch (expected: IllegalArgumentException) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import io.novafoundation.nova.common.utils.findIsInstanceOrNull
|
||||
import io.novafoundation.nova.common.utils.formatNamed
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.common.utils.emptyTronAccountId
|
||||
import io.novafoundation.nova.common.utils.isValidTronAddress
|
||||
import io.novafoundation.nova.common.utils.substrateAccountId
|
||||
import io.novafoundation.nova.common.utils.toTronAddress
|
||||
import io.novafoundation.nova.common.utils.tronAddressToAccountId
|
||||
@@ -361,13 +362,19 @@ fun Chain.multiAddressOf(accountId: ByteArray): MultiAddress {
|
||||
|
||||
fun Chain.isValidAddress(address: String): Boolean {
|
||||
return runCatching {
|
||||
if (isEthereumBased) {
|
||||
address.asEthereumAddress().isValid()
|
||||
} else {
|
||||
address.toAccountId() // verify supplied address can be converted to account id
|
||||
when {
|
||||
// Tron addresses are Base58Check(0x41 ++ accountId), not SS58 or plain 0x-hex - neither of the two
|
||||
// branches below would ever accept them, so this needs its own dedicated check.
|
||||
isTronBased -> address.isValidTronAddress()
|
||||
|
||||
addressPrefix.toShort() == address.addressPrefix() ||
|
||||
legacyAddressPrefix?.toShort() == address.addressPrefix()
|
||||
isEthereumBased -> address.asEthereumAddress().isValid()
|
||||
|
||||
else -> {
|
||||
address.toAccountId() // verify supplied address can be converted to account id
|
||||
|
||||
addressPrefix.toShort() == address.addressPrefix() ||
|
||||
legacyAddressPrefix?.toShort() == address.addressPrefix()
|
||||
}
|
||||
}
|
||||
}.getOrDefault(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user