mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 09:05:48 +00:00
Add read-only Tron (TRC20) chain family support (#9)
* Add read-only Tron (TRC20) chain family support Adds Tron as a third chain family alongside Substrate and EVM: address derivation (BIP44 coin type 195, secp256k1 reused from the existing Ethereum path, Base58Check encoding), TronGrid-backed TRX and TRC20 USDT balance display, and a Room migration for the new per-account Tron key/address columns. Read-only only — no send, signing, or broadcast support yet. * fix: exhaustive when branches for Trc20/TronNative asset types ChainExt.kt's onChainAssetId and Common.kt's existentialDepositError both switch exhaustively over Chain.Asset.Type and didn't have cases for the new Trc20/TronNative variants, breaking compilation. Trc20 mirrors EvmErc20 (contract address as the on-chain id, dust burns rather than transfers on removal); TronNative mirrors EvmNative.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
Next generation mobile wallet for Pezkuwichain and the Polkadot ecosystem.
|
||||
|
||||
[](https://twitter.com/pezkuwichain)
|
||||
[](https://x.com/bizinikiwi)
|
||||
|
||||
## About
|
||||
|
||||
@@ -105,8 +105,8 @@ WALLET_CONNECT_PROJECT_ID=mock
|
||||
|
||||
- Website: https://pezkuwichain.io
|
||||
- Documentation: https://docs.pezkuwichain.io
|
||||
- Telegram: https://t.me/pezkuwichain
|
||||
- Twitter: https://twitter.com/pezkuwichain
|
||||
- Telegram: https://t.me/kurdishmedya
|
||||
- Twitter: https://x.com/bizinikiwi
|
||||
- GitHub: https://github.com/pezkuwichain
|
||||
|
||||
## License
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ android {
|
||||
buildConfigField "String", "TERMS_URL", "\"https://pezkuwichain.io/terms.html\""
|
||||
buildConfigField "String", "GITHUB_URL", "\"https://github.com/pezkuwichain\""
|
||||
buildConfigField "String", "TELEGRAM_URL", "\"https://t.me/pezkuwichainBot\""
|
||||
buildConfigField "String", "TWITTER_URL", "\"https://twitter.com/pezkuwichain\""
|
||||
buildConfigField "String", "TWITTER_URL", "\"https://x.com/bizinikiwi\""
|
||||
buildConfigField "String", "RATE_URL", "\"market://details?id=${rootProject.applicationId}.${releaseApplicationSuffix}\""
|
||||
buildConfigField "String", "EMAIL", "\"support@pezkuwichain.io\""
|
||||
buildConfigField "String", "YOUTUBE_URL", "\"https://www.youtube.com/@SatoshiQazi\""
|
||||
|
||||
+20
@@ -25,6 +25,9 @@ object MetaAccountSecrets : Schema<MetaAccountSecrets>() {
|
||||
|
||||
val EthereumKeypair by schema(KeyPairSchema).optional()
|
||||
val EthereumDerivationPath by string().optional()
|
||||
|
||||
val TronKeypair by schema(KeyPairSchema).optional()
|
||||
val TronDerivationPath by string().optional()
|
||||
}
|
||||
|
||||
object ChainAccountSecrets : Schema<ChainAccountSecrets>() {
|
||||
@@ -42,6 +45,8 @@ fun MetaAccountSecrets(
|
||||
substrateDerivationPath: String? = null,
|
||||
ethereumKeypair: Keypair? = null,
|
||||
ethereumDerivationPath: String? = null,
|
||||
tronKeypair: Keypair? = null,
|
||||
tronDerivationPath: String? = null,
|
||||
): EncodableStruct<MetaAccountSecrets> = MetaAccountSecrets { secrets ->
|
||||
secrets[Entropy] = entropy
|
||||
secrets[SubstrateSeed] = substrateSeed
|
||||
@@ -61,6 +66,15 @@ fun MetaAccountSecrets(
|
||||
}
|
||||
}
|
||||
secrets[EthereumDerivationPath] = ethereumDerivationPath
|
||||
|
||||
secrets[TronKeypair] = tronKeypair?.let {
|
||||
KeyPairSchema { keypair ->
|
||||
keypair[PublicKey] = it.publicKey
|
||||
keypair[PrivateKey] = it.privateKey
|
||||
keypair[Nonce] = null // tron uses secp256k1 (like ethereum), so nonce is always null
|
||||
}
|
||||
}
|
||||
secrets[TronDerivationPath] = tronDerivationPath
|
||||
}
|
||||
|
||||
fun ChainAccountSecrets(
|
||||
@@ -86,6 +100,9 @@ val EncodableStruct<MetaAccountSecrets>.substrateDerivationPath
|
||||
val EncodableStruct<MetaAccountSecrets>.ethereumDerivationPath
|
||||
get() = get(MetaAccountSecrets.EthereumDerivationPath)
|
||||
|
||||
val EncodableStruct<MetaAccountSecrets>.tronDerivationPath
|
||||
get() = get(MetaAccountSecrets.TronDerivationPath)
|
||||
|
||||
val EncodableStruct<MetaAccountSecrets>.entropy
|
||||
get() = get(MetaAccountSecrets.Entropy)
|
||||
|
||||
@@ -98,6 +115,9 @@ val EncodableStruct<MetaAccountSecrets>.substrateKeypair
|
||||
val EncodableStruct<MetaAccountSecrets>.ethereumKeypair
|
||||
get() = get(MetaAccountSecrets.EthereumKeypair)
|
||||
|
||||
val EncodableStruct<MetaAccountSecrets>.tronKeypair
|
||||
get() = get(MetaAccountSecrets.TronKeypair)
|
||||
|
||||
val EncodableStruct<ChainAccountSecrets>.derivationPath
|
||||
get() = get(ChainAccountSecrets.DerivationPath)
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
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.runtime.AccountId
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* Tron address format:
|
||||
* Base58Check(0x41 ++ accountId), where `accountId` is the 20-byte keccak256-derived id
|
||||
* (the exact same derivation Ethereum uses: keccak256(uncompressed pubkey without the 0x04 prefix)[-20:]).
|
||||
*
|
||||
* We deliberately reuse the existing Ethereum-style pubkey -> accountId derivation from `substrate_sdk_android`
|
||||
* (`asEthereumPublicKey().toAccountId()`) instead of re-implementing keccak/secp256k1 ourselves, since that math
|
||||
* is identical for Tron - only the final string encoding differs (Base58Check with a 0x41 prefix, instead of
|
||||
* checksummed hex with a 0x prefix).
|
||||
*
|
||||
* Base58Check itself (this file's [Base58]/[Base58Check] objects) is hand-implemented since no Base58 library is
|
||||
* currently on this project's classpath. It is a deterministic text encoding (not a secret-dependent cryptographic
|
||||
* primitive) and has been cross-checked against the well-known USDT-TRC20 contract address
|
||||
* (TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t <-> hex 41a614f803b6fd780986a42c78ec9c7f77e6ded13c) - see [TronAddressTest].
|
||||
*/
|
||||
private const val TRON_ADDRESS_PREFIX_BYTE: Byte = 0x41
|
||||
|
||||
object Base58 {
|
||||
|
||||
private const val ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
private val BASE = BigInteger.valueOf(58)
|
||||
|
||||
fun encode(input: ByteArray): String {
|
||||
if (input.isEmpty()) return ""
|
||||
|
||||
var value = BigInteger(1, input)
|
||||
|
||||
val sb = StringBuilder()
|
||||
while (value > BigInteger.ZERO) {
|
||||
val (div, rem) = value.divideAndRemainder(BASE)
|
||||
sb.append(ALPHABET[rem.toInt()])
|
||||
value = div
|
||||
}
|
||||
|
||||
// Every leading zero byte of the input must be represented as a leading '1' in the output,
|
||||
// since a plain big-integer encoding would otherwise drop them.
|
||||
val leadingZeroBytes = input.takeWhile { it == 0.toByte() }.size
|
||||
repeat(leadingZeroBytes) { sb.append(ALPHABET[0]) }
|
||||
|
||||
return sb.reverse().toString()
|
||||
}
|
||||
|
||||
fun decode(input: String): ByteArray {
|
||||
if (input.isEmpty()) return ByteArray(0)
|
||||
|
||||
var value = BigInteger.ZERO
|
||||
for (char in input) {
|
||||
val digit = ALPHABET.indexOf(char)
|
||||
require(digit >= 0) { "Invalid Base58 character: '$char'" }
|
||||
|
||||
value = value.multiply(BASE).add(BigInteger.valueOf(digit.toLong()))
|
||||
}
|
||||
|
||||
var bytes = if (value == BigInteger.ZERO) ByteArray(0) else value.toByteArray()
|
||||
// BigInteger#toByteArray may prepend a single zero sign byte for an otherwise-positive value; drop it.
|
||||
if (bytes.size > 1 && bytes[0] == 0.toByte()) {
|
||||
bytes = bytes.copyOfRange(1, bytes.size)
|
||||
}
|
||||
|
||||
val leadingOnes = input.takeWhile { it == ALPHABET[0] }.length
|
||||
|
||||
return ByteArray(leadingOnes) + bytes
|
||||
}
|
||||
}
|
||||
|
||||
object Base58Check {
|
||||
|
||||
private const val CHECKSUM_SIZE = 4
|
||||
|
||||
fun encode(payload: ByteArray): String {
|
||||
val checksum = payload.sha256().sha256().copyOfRange(0, CHECKSUM_SIZE)
|
||||
|
||||
return Base58.encode(payload + checksum)
|
||||
}
|
||||
|
||||
fun decode(input: String): ByteArray {
|
||||
val full = Base58.decode(input)
|
||||
require(full.size > CHECKSUM_SIZE) { "Base58Check payload too short: $input" }
|
||||
|
||||
val payload = full.copyOfRange(0, full.size - CHECKSUM_SIZE)
|
||||
val checksum = full.copyOfRange(full.size - CHECKSUM_SIZE, full.size)
|
||||
|
||||
val expectedChecksum = payload.sha256().sha256().copyOfRange(0, CHECKSUM_SIZE)
|
||||
require(checksum.contentEquals(expectedChecksum)) { "Invalid Base58Check checksum: $input" }
|
||||
|
||||
return payload
|
||||
}
|
||||
}
|
||||
|
||||
// See the file-level doc above for why it's correct to reuse the Ethereum pubkey->accountId derivation here.
|
||||
fun ByteArray.tronPublicKeyToAccountId(): AccountId = asEthereumPublicKey().toAccountId().value
|
||||
|
||||
fun AccountId.toTronAddress(): String {
|
||||
require(size == 20) { "Tron account id must be 20 bytes, got $size" }
|
||||
|
||||
return Base58Check.encode(byteArrayOf(TRON_ADDRESS_PREFIX_BYTE) + this)
|
||||
}
|
||||
|
||||
fun String.tronAddressToAccountId(): AccountId {
|
||||
val decoded = Base58Check.decode(this)
|
||||
require(decoded.size == 21 && decoded[0] == TRON_ADDRESS_PREFIX_BYTE) { "Not a valid Tron address: $this" }
|
||||
|
||||
return decoded.copyOfRange(1, decoded.size)
|
||||
}
|
||||
|
||||
fun String.isValidTronAddress(): Boolean = runCatching { tronAddressToAccountId() }.isSuccess
|
||||
|
||||
fun emptyTronAccountId() = ByteArray(20) { 1 }
|
||||
@@ -0,0 +1,79 @@
|
||||
package io.novafoundation.nova.common.utils
|
||||
|
||||
import io.novasama.substrate_sdk_android.extensions.fromHex
|
||||
import io.novasama.substrate_sdk_android.extensions.toHexString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class TronAddressTest {
|
||||
|
||||
/**
|
||||
* TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t is the well-known Tron mainnet USDT (TRC-20) contract address
|
||||
* (the same one used in this app's Tron chain config). Its raw 21-byte Base58Check payload
|
||||
* (0x41 prefix ++ 20-byte account id) is publicly documented as
|
||||
* 41a614f803b6fd780986a42c78ec9c7f77e6ded13c - this is an independently-verifiable, real-world
|
||||
* test vector (not a value invented for this test).
|
||||
*/
|
||||
private val knownTronAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"
|
||||
private val knownTronAddressHex = "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"
|
||||
|
||||
@Test
|
||||
fun `should decode known Tron address to expected raw bytes`() {
|
||||
val decodedPayload = Base58Check.decode(knownTronAddress)
|
||||
|
||||
assertEquals(knownTronAddressHex, decodedPayload.toHexString(withPrefix = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should re-encode known raw bytes back to the exact known Tron address`() {
|
||||
val payload = knownTronAddressHex.fromHex()
|
||||
|
||||
assertEquals(knownTronAddress, Base58Check.encode(payload))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accountId to address and back should round trip`() {
|
||||
val accountId = knownTronAddressHex.fromHex().copyOfRange(1, 21)
|
||||
|
||||
val address = accountId.toTronAddress()
|
||||
assertEquals(knownTronAddress, address)
|
||||
|
||||
val decodedBack = address.tronAddressToAccountId()
|
||||
assertTrue(decodedBack.contentEquals(accountId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidTronAddress should accept known good address`() {
|
||||
assertTrue(knownTronAddress.isValidTronAddress())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidTronAddress should reject corrupted address`() {
|
||||
val corrupted = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6u" // last char changed -> checksum mismatch
|
||||
assertFalse(corrupted.isValidTronAddress())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isValidTronAddress should reject a plain Ethereum-style address`() {
|
||||
assertFalse("0x9858EfFD232B4033E47d90003D41EC34EcaEda94".isValidTronAddress())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Base58 encode should preserve leading zero bytes as leading 1s`() {
|
||||
val input = byteArrayOf(0, 0, 1, 2, 3)
|
||||
|
||||
val encoded = Base58.encode(input)
|
||||
assertTrue(encoded.startsWith("11"))
|
||||
|
||||
val decoded = Base58.decode(encoded)
|
||||
assertTrue(decoded.contentEquals(input))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Base58 encode-decode should round trip for empty input`() {
|
||||
assertEquals("", Base58.encode(ByteArray(0)))
|
||||
assertTrue(Base58.decode("").isEmpty())
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ fun chainOf(
|
||||
legacyPrefix = null,
|
||||
isTestNet = false,
|
||||
isEthereumBased = false,
|
||||
isTronBased = false,
|
||||
hasCrowdloans = false,
|
||||
additional = "",
|
||||
governance = "governance",
|
||||
|
||||
@@ -94,6 +94,7 @@ import io.novafoundation.nova.core_db.migrations.AddStakingTypeToTotalRewards_44
|
||||
import io.novafoundation.nova.core_db.migrations.AddSwapOption_48_49
|
||||
import io.novafoundation.nova.core_db.migrations.AddTransactionVersionToRuntime_50_51
|
||||
import io.novafoundation.nova.core_db.migrations.AddTransferApisTable_29_30
|
||||
import io.novafoundation.nova.core_db.migrations.AddTronSupport_73_74
|
||||
import io.novafoundation.nova.core_db.migrations.AddTypeExtrasToMetaAccount_68_69
|
||||
import io.novafoundation.nova.core_db.migrations.AddVersioningToGovernanceDapps_32_33
|
||||
import io.novafoundation.nova.core_db.migrations.AddWalletConnectSessions_39_40
|
||||
@@ -166,7 +167,7 @@ import io.novafoundation.nova.core_db.model.operation.SwapTypeLocal
|
||||
import io.novafoundation.nova.core_db.model.operation.TransferTypeLocal
|
||||
|
||||
@Database(
|
||||
version = 73,
|
||||
version = 74,
|
||||
entities = [
|
||||
AccountLocal::class,
|
||||
NodeLocal::class,
|
||||
@@ -271,6 +272,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
.addMigrations(AddFavoriteDAppsOrdering_65_66, AddLegacyAddressPrefix_66_67, AddSellProviders_67_68)
|
||||
.addMigrations(AddTypeExtrasToMetaAccount_68_69, AddMultisigCalls_69_70, AddMultisigSupportFlag_70_71)
|
||||
.addMigrations(AddGifts_71_72, AddFieldsToContributions)
|
||||
.addMigrations(AddTronSupport_73_74)
|
||||
.build()
|
||||
}
|
||||
return instance!!
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.core_db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
val AddTronSupport_73_74 = object : Migration(73, 74) {
|
||||
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("ALTER TABLE chains ADD COLUMN isTronBased INTEGER NOT NULL DEFAULT 0")
|
||||
|
||||
db.execSQL("ALTER TABLE meta_accounts ADD COLUMN tronPublicKey BLOB")
|
||||
db.execSQL("ALTER TABLE meta_accounts ADD COLUMN tronAddress BLOB")
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ data class ChainLocal(
|
||||
val prefix: Int,
|
||||
val legacyPrefix: Int?,
|
||||
val isEthereumBased: Boolean,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
val isTronBased: Boolean,
|
||||
val isTestNet: Boolean,
|
||||
@ColumnInfo(defaultValue = "1")
|
||||
val hasSubstrateRuntime: Boolean,
|
||||
|
||||
+13
-2
@@ -36,7 +36,9 @@ class MetaAccountLocal(
|
||||
@ColumnInfo(defaultValue = "ACTIVE")
|
||||
val status: Status,
|
||||
val globallyUniqueId: String,
|
||||
val typeExtras: SerializedJson?
|
||||
val typeExtras: SerializedJson?,
|
||||
val tronPublicKey: ByteArray? = null,
|
||||
val tronAddress: ByteArray? = null,
|
||||
) {
|
||||
|
||||
enum class Status {
|
||||
@@ -54,6 +56,9 @@ class MetaAccountLocal(
|
||||
const val ETHEREUM_PUBKEY = "ethereumPublicKey"
|
||||
const val ETHEREUM_ADDRESS = "ethereumAddress"
|
||||
|
||||
const val TRON_PUBKEY = "tronPublicKey"
|
||||
const val TRON_ADDRESS = "tronAddress"
|
||||
|
||||
const val NAME = "name"
|
||||
const val IS_SELECTED = "isSelected"
|
||||
const val POSITION = "position"
|
||||
@@ -83,7 +88,9 @@ class MetaAccountLocal(
|
||||
type = type,
|
||||
status = status,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
typeExtras = typeExtras
|
||||
typeExtras = typeExtras,
|
||||
tronPublicKey = tronPublicKey,
|
||||
tronAddress = tronAddress
|
||||
).also {
|
||||
it.id = id
|
||||
}
|
||||
@@ -100,6 +107,8 @@ class MetaAccountLocal(
|
||||
if (!substrateAccountId.contentEquals(other.substrateAccountId)) return false
|
||||
if (!ethereumPublicKey.contentEquals(other.ethereumPublicKey)) return false
|
||||
if (!ethereumAddress.contentEquals(other.ethereumAddress)) return false
|
||||
if (!tronPublicKey.contentEquals(other.tronPublicKey)) return false
|
||||
if (!tronAddress.contentEquals(other.tronAddress)) return false
|
||||
if (name != other.name) return false
|
||||
if (parentMetaId != other.parentMetaId) return false
|
||||
if (isSelected != other.isSelected) return false
|
||||
@@ -119,6 +128,8 @@ class MetaAccountLocal(
|
||||
result = 31 * result + (substrateAccountId?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (ethereumPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (ethereumAddress?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (tronPublicKey?.contentHashCode() ?: 0)
|
||||
result = 31 * result + (tronAddress?.contentHashCode() ?: 0)
|
||||
result = 31 * result + name.hashCode()
|
||||
result = 31 * result + (parentMetaId?.hashCode() ?: 0)
|
||||
result = 31 * result + isSelected.hashCode()
|
||||
|
||||
+12
-1
@@ -45,6 +45,13 @@ interface LightMetaAccount {
|
||||
val substrateAccountId: ByteArray?
|
||||
val ethereumAddress: ByteArray?
|
||||
val ethereumPublicKey: ByteArray?
|
||||
|
||||
/**
|
||||
* Tron account id (same keccak-based derivation as [ethereumAddress], different derivation path and address encoding).
|
||||
* Only populated for [Type.SECRETS] wallets today (Phase 1 read-only support) - null for every other wallet type.
|
||||
*/
|
||||
val tronAddress: ByteArray?
|
||||
val tronPublicKey: ByteArray?
|
||||
val isSelected: Boolean
|
||||
val name: String
|
||||
val type: Type
|
||||
@@ -81,6 +88,8 @@ fun LightMetaAccount(
|
||||
type: LightMetaAccount.Type,
|
||||
status: LightMetaAccount.Status,
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
) = object : LightMetaAccount {
|
||||
override val id: Long = id
|
||||
override val globallyUniqueId: String = globallyUniqueId
|
||||
@@ -89,6 +98,8 @@ fun LightMetaAccount(
|
||||
override val substrateAccountId: ByteArray? = substrateAccountId
|
||||
override val ethereumAddress: ByteArray? = ethereumAddress
|
||||
override val ethereumPublicKey: ByteArray? = ethereumPublicKey
|
||||
override val tronAddress: ByteArray? = tronAddress
|
||||
override val tronPublicKey: ByteArray? = tronPublicKey
|
||||
override val isSelected: Boolean = isSelected
|
||||
override val name: String = name
|
||||
override val type: LightMetaAccount.Type = type
|
||||
@@ -155,7 +166,7 @@ sealed class MultisigAvailability {
|
||||
}
|
||||
|
||||
fun MetaAccount.isUniversal(): Boolean {
|
||||
return substrateAccountId != null || ethereumAddress != null
|
||||
return substrateAccountId != null || ethereumAddress != null || tronAddress != null
|
||||
}
|
||||
|
||||
fun MultisigAvailability.singleChainId(): ChainId? {
|
||||
|
||||
+14
@@ -63,6 +63,8 @@ class AccountMappers(
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumAddress = ethereumAddress,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
status = mapMetaAccountStateFromLocal(status),
|
||||
@@ -78,6 +80,8 @@ class AccountMappers(
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumAddress = ethereumAddress,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -95,6 +99,8 @@ class AccountMappers(
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumAddress = ethereumAddress,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -111,6 +117,8 @@ class AccountMappers(
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumAddress = ethereumAddress,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -128,6 +136,8 @@ class AccountMappers(
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumAddress = ethereumAddress,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
type = type,
|
||||
@@ -153,6 +163,8 @@ class AccountMappers(
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumAddress = ethereumAddress,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
status = mapMetaAccountStateFromLocal(status),
|
||||
@@ -169,6 +181,8 @@ class AccountMappers(
|
||||
substrateAccountId = substrateAccountId,
|
||||
ethereumAddress = ethereumAddress,
|
||||
ethereumPublicKey = ethereumPublicKey,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey,
|
||||
chainAccounts = chainAccounts,
|
||||
isSelected = isSelected,
|
||||
name = name,
|
||||
|
||||
+5
-1
@@ -3,6 +3,7 @@ package io.novafoundation.nova.feature_account_impl.data.repository.datasource
|
||||
import io.novafoundation.nova.common.data.secrets.v2.KeyPairSchema
|
||||
import io.novafoundation.nova.common.data.secrets.v2.MetaAccountSecrets
|
||||
import io.novafoundation.nova.common.utils.substrateAccountId
|
||||
import io.novafoundation.nova.common.utils.tronPublicKeyToAccountId
|
||||
import io.novafoundation.nova.core.model.CryptoType
|
||||
import io.novafoundation.nova.core_db.model.chain.account.MetaAccountLocal
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumPublicKey
|
||||
@@ -29,6 +30,7 @@ class RealSecretsMetaAccountLocalFactory : SecretsMetaAccountLocalFactory {
|
||||
): MetaAccountLocal {
|
||||
val substratePublicKey = secrets[MetaAccountSecrets.SubstrateKeypair][KeyPairSchema.PublicKey]
|
||||
val ethereumPublicKey = secrets[MetaAccountSecrets.EthereumKeypair]?.get(KeyPairSchema.PublicKey)
|
||||
val tronPublicKey = secrets[MetaAccountSecrets.TronKeypair]?.get(KeyPairSchema.PublicKey)
|
||||
|
||||
return MetaAccountLocal(
|
||||
substratePublicKey = substratePublicKey,
|
||||
@@ -43,7 +45,9 @@ class RealSecretsMetaAccountLocalFactory : SecretsMetaAccountLocalFactory {
|
||||
type = MetaAccountLocal.Type.SECRETS,
|
||||
status = MetaAccountLocal.Status.ACTIVE,
|
||||
globallyUniqueId = MetaAccountLocal.generateGloballyUniqueId(),
|
||||
typeExtras = null
|
||||
typeExtras = null,
|
||||
tronPublicKey = tronPublicKey,
|
||||
tronAddress = tronPublicKey?.tronPublicKeyToAccountId()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+21
-1
@@ -28,6 +28,12 @@ import io.novasama.substrate_sdk_android.scale.Schema
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* SLIP-44 coin type 195 is Tron's registered BIP44 coin type (Ethereum's is 60).
|
||||
* Not user-configurable via the "Advanced Encryption" UI yet (Phase 1 read-only Tron support) - always derived at this fixed path.
|
||||
*/
|
||||
const val TRON_DEFAULT_DERIVATION_PATH = "//44//195//0/0/0"
|
||||
|
||||
class AccountSecretsFactory(
|
||||
private val JsonDecoder: JsonDecoder
|
||||
) {
|
||||
@@ -124,6 +130,7 @@ class AccountSecretsFactory(
|
||||
substrateDerivationPath: String?,
|
||||
ethereumDerivationPath: String?,
|
||||
accountSource: AccountSource,
|
||||
tronDerivationPath: String? = TRON_DEFAULT_DERIVATION_PATH,
|
||||
): Result<MetaAccountSecrets> = withContext(Dispatchers.Default) {
|
||||
val (substrateSecrets, substrateCryptoType) = chainAccountSecrets(
|
||||
derivationPath = substrateDerivationPath,
|
||||
@@ -139,13 +146,26 @@ class AccountSecretsFactory(
|
||||
Bip32EcdsaKeypairFactory.generate(seed = seed, junctions = decodedEthereumDerivationPath?.junctions.orEmpty())
|
||||
}
|
||||
|
||||
// Tron uses the same secp256k1/BIP32 keypair generation as Ethereum, just under its own SLIP-44 coin-type (195)
|
||||
// derivation path, so it always yields a different keypair from the Ethereum one above, even though the math is identical.
|
||||
val tronKeypair = accountSource.castOrNull<AccountSource.Mnemonic>()?.let {
|
||||
// "Ethereum" here just means "BIP32/ECDSA junction decoding", which is exactly what Tron's path also needs.
|
||||
val decodedTronDerivationPath = decodeDerivationPath(tronDerivationPath, ethereum = true)
|
||||
|
||||
val seed = deriveSeed(it.mnemonic, password = decodedTronDerivationPath?.password, ethereum = true).seed
|
||||
|
||||
Bip32EcdsaKeypairFactory.generate(seed = seed, junctions = decodedTronDerivationPath?.junctions.orEmpty())
|
||||
}
|
||||
|
||||
val secrets = MetaAccountSecrets(
|
||||
entropy = substrateSecrets[ChainAccountSecrets.Entropy],
|
||||
substrateSeed = substrateSecrets[ChainAccountSecrets.Seed],
|
||||
substrateKeyPair = mapKeypairStructToKeypair(substrateSecrets[ChainAccountSecrets.Keypair]),
|
||||
substrateDerivationPath = substrateDerivationPath,
|
||||
ethereumKeypair = ethereumKeypair,
|
||||
ethereumDerivationPath = ethereumDerivationPath
|
||||
ethereumDerivationPath = ethereumDerivationPath,
|
||||
tronKeypair = tronKeypair,
|
||||
tronDerivationPath = tronDerivationPath
|
||||
)
|
||||
|
||||
Result(secrets = secrets, cryptoType = substrateCryptoType)
|
||||
|
||||
+6
-1
@@ -21,7 +21,9 @@ open class DefaultMetaAccount(
|
||||
override val type: LightMetaAccount.Type,
|
||||
override val status: LightMetaAccount.Status,
|
||||
override val chainAccounts: Map<ChainId, MetaAccount.ChainAccount>,
|
||||
override val parentMetaId: Long?
|
||||
override val parentMetaId: Long?,
|
||||
override val tronAddress: ByteArray? = null,
|
||||
override val tronPublicKey: ByteArray? = null,
|
||||
) : MetaAccount {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
@@ -31,6 +33,7 @@ open class DefaultMetaAccount(
|
||||
override fun hasAccountIn(chain: Chain): Boolean {
|
||||
return when {
|
||||
hasChainAccountIn(chain.id) -> true
|
||||
chain.isTronBased -> tronAddress != null
|
||||
chain.isEthereumBased -> ethereumAddress != null
|
||||
else -> substrateAccountId != null
|
||||
}
|
||||
@@ -39,6 +42,7 @@ open class DefaultMetaAccount(
|
||||
override fun accountIdIn(chain: Chain): AccountId? {
|
||||
return when {
|
||||
hasChainAccountIn(chain.id) -> chainAccounts.getValue(chain.id).accountId
|
||||
chain.isTronBased -> tronAddress
|
||||
chain.isEthereumBased -> ethereumAddress
|
||||
else -> substrateAccountId
|
||||
}
|
||||
@@ -47,6 +51,7 @@ open class DefaultMetaAccount(
|
||||
override fun publicKeyIn(chain: Chain): ByteArray? {
|
||||
return when {
|
||||
hasChainAccountIn(chain.id) -> chainAccounts.getValue(chain.id).publicKey
|
||||
chain.isTronBased -> tronPublicKey
|
||||
chain.isEthereumBased -> ethereumPublicKey
|
||||
else -> substratePublicKey
|
||||
}
|
||||
|
||||
+6
-2
@@ -21,7 +21,9 @@ class GenericLedgerMetaAccount(
|
||||
status: LightMetaAccount.Status,
|
||||
chainAccounts: Map<ChainId, MetaAccount.ChainAccount>,
|
||||
parentMetaId: Long?,
|
||||
private val supportedGenericLedgerChains: Set<ChainId>
|
||||
private val supportedGenericLedgerChains: Set<ChainId>,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -35,7 +37,9 @@ class GenericLedgerMetaAccount(
|
||||
type = type,
|
||||
status = status,
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
) {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
|
||||
+6
-2
@@ -21,7 +21,9 @@ class LegacyLedgerMetaAccount(
|
||||
type: LightMetaAccount.Type,
|
||||
status: LightMetaAccount.Status,
|
||||
chainAccounts: Map<ChainId, MetaAccount.ChainAccount>,
|
||||
parentMetaId: Long?
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -35,7 +37,9 @@ class LegacyLedgerMetaAccount(
|
||||
type = type,
|
||||
status = status,
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
) {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
|
||||
+6
-2
@@ -27,7 +27,9 @@ class RealMultisigMetaAccount(
|
||||
private val otherSignatoriesUnsorted: List<AccountIdKey>,
|
||||
override val threshold: Int,
|
||||
private val multisigRepository: MultisigRepository,
|
||||
parentMetaId: Long?
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -41,7 +43,9 @@ class RealMultisigMetaAccount(
|
||||
type = LightMetaAccount.Type.MULTISIG,
|
||||
status = status,
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
),
|
||||
MultisigMetaAccount {
|
||||
|
||||
|
||||
+7
-3
@@ -19,7 +19,9 @@ class PolkadotVaultMetaAccount(
|
||||
type: LightMetaAccount.Type,
|
||||
status: LightMetaAccount.Status,
|
||||
chainAccounts: Map<ChainId, MetaAccount.ChainAccount>,
|
||||
parentMetaId: Long?
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -33,10 +35,12 @@ class PolkadotVaultMetaAccount(
|
||||
type = type,
|
||||
status = status,
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
) {
|
||||
|
||||
override suspend fun supportsAddingChainAccount(chain: Chain): Boolean {
|
||||
return !chain.isEthereumBased
|
||||
return !chain.isEthereumBased && !chain.isTronBased
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -21,7 +21,9 @@ internal class RealProxiedMetaAccount(
|
||||
status: LightMetaAccount.Status,
|
||||
override val proxy: ProxyAccount,
|
||||
chainAccounts: Map<ChainId, MetaAccount.ChainAccount>,
|
||||
parentMetaId: Long?
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -35,7 +37,9 @@ internal class RealProxiedMetaAccount(
|
||||
type = LightMetaAccount.Type.PROXIED,
|
||||
status = status,
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
),
|
||||
ProxiedMetaAccount {
|
||||
|
||||
|
||||
+6
-2
@@ -22,7 +22,9 @@ class RealSecretsMetaAccount(
|
||||
name: String,
|
||||
status: LightMetaAccount.Status,
|
||||
chainAccounts: Map<ChainId, MetaAccount.ChainAccount>,
|
||||
parentMetaId: Long?
|
||||
parentMetaId: Long?,
|
||||
tronAddress: ByteArray? = null,
|
||||
tronPublicKey: ByteArray? = null,
|
||||
) : DefaultMetaAccount(
|
||||
id = id,
|
||||
globallyUniqueId = globallyUniqueId,
|
||||
@@ -36,7 +38,9 @@ class RealSecretsMetaAccount(
|
||||
type = LightMetaAccount.Type.SECRETS,
|
||||
status = status,
|
||||
chainAccounts = chainAccounts,
|
||||
parentMetaId = parentMetaId
|
||||
parentMetaId = parentMetaId,
|
||||
tronAddress = tronAddress,
|
||||
tronPublicKey = tronPublicKey
|
||||
),
|
||||
SecretsMetaAccount {
|
||||
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.novafoundation.nova.feature_account_impl.data
|
||||
|
||||
import io.novafoundation.nova.common.utils.DEFAULT_DERIVATION_PATH
|
||||
import io.novafoundation.nova.common.utils.ethereumAddressToAccountId
|
||||
import io.novafoundation.nova.common.utils.toTronAddress
|
||||
import io.novafoundation.nova.common.utils.tronPublicKeyToAccountId
|
||||
import io.novafoundation.nova.feature_account_impl.data.secrets.TRON_DEFAULT_DERIVATION_PATH
|
||||
import io.novasama.substrate_sdk_android.encrypt.junction.BIP32JunctionDecoder
|
||||
import io.novasama.substrate_sdk_android.encrypt.keypair.bip32.Bip32EcdsaKeypairFactory
|
||||
import io.novasama.substrate_sdk_android.encrypt.keypair.bip32.generate
|
||||
import io.novasama.substrate_sdk_android.encrypt.seed.bip39.Bip39SeedFactory
|
||||
import io.novasama.substrate_sdk_android.extensions.asEthereumPublicKey
|
||||
import io.novasama.substrate_sdk_android.extensions.toAccountId
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Cross-checks Tron address derivation (Phase 1, read-only support) against values independently computed
|
||||
* outside this codebase (Python: hashlib/ecdsa/pycryptodome, none of which are used by the app itself) for the
|
||||
* standard BIP39 test mnemonic "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon
|
||||
* abandon about".
|
||||
*
|
||||
* The Ethereum test case is included first as a control: it reproduces the exact same well-known reference
|
||||
* address for this mnemonic that ethers.js/MetaMask documentation uses
|
||||
* (0x9858EfFD232B4033E47d90003D41EC34EcaEda94 at m/44'/60'/0'/0/0), which validates that this project's BIP32 +
|
||||
* secp256k1 + keccak pipeline (`Bip32EcdsaKeypairFactory` + `asEthereumPublicKey().toAccountId()`, from
|
||||
* `substrate_sdk_android`) behaves as expected. The Tron test case then reuses the exact same pipeline under
|
||||
* Tron's own SLIP-44 coin-type-195 derivation path, which necessarily yields a different keypair (and therefore
|
||||
* a different address) even though the math is identical - this is expected, not a bug.
|
||||
*/
|
||||
class TronDerivationTest {
|
||||
|
||||
private val testMnemonic =
|
||||
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
@Test
|
||||
fun `should derive well-known reference Ethereum address for the standard test mnemonic`() {
|
||||
val expectedAccountId = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94".ethereumAddressToAccountId()
|
||||
|
||||
val seed = Bip39SeedFactory.deriveSeed(testMnemonic, password = null)
|
||||
val keypair = Bip32EcdsaKeypairFactory.generate(seed.seed, BIP32JunctionDecoder.DEFAULT_DERIVATION_PATH)
|
||||
|
||||
val actualAccountId = keypair.publicKey.asEthereumPublicKey().toAccountId().value
|
||||
|
||||
assertArrayEquals(expectedAccountId, actualAccountId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should derive Tron address for the standard test mnemonic at the coin-195 path`() {
|
||||
// Independently verified via a from-scratch Python implementation (hashlib SHA-256 for Base58Check,
|
||||
// pycryptodome Keccak-256, `ecdsa` for secp256k1/BIP32) - see task verification notes.
|
||||
val expectedTronAddress = "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH"
|
||||
|
||||
val seed = Bip39SeedFactory.deriveSeed(testMnemonic, password = null)
|
||||
val keypair = Bip32EcdsaKeypairFactory.generate(seed.seed, TRON_DEFAULT_DERIVATION_PATH)
|
||||
|
||||
val accountId = keypair.publicKey.tronPublicKeyToAccountId()
|
||||
val actualTronAddress = accountId.toTronAddress()
|
||||
|
||||
assertEquals(expectedTronAddress, actualTronAddress)
|
||||
}
|
||||
}
|
||||
+1
@@ -124,6 +124,7 @@ class CustomChainFactory(
|
||||
legacyAddressPrefix = null,
|
||||
types = prefilledChain?.types,
|
||||
isEthereumBased = isEthereumBased,
|
||||
isTronBased = false,
|
||||
isTestNet = prefilledChain?.isTestNet.orFalse(),
|
||||
source = Chain.Source.CUSTOM,
|
||||
hasSubstrateRuntime = hasSubstrateRuntime,
|
||||
|
||||
+8
@@ -29,6 +29,8 @@ class TypeBasedAssetSourceRegistry(
|
||||
private val evmErc20Source: Lazy<AssetSource>,
|
||||
private val evmNativeSource: Lazy<AssetSource>,
|
||||
private val equilibriumAssetSource: Lazy<AssetSource>,
|
||||
private val tronNativeSource: Lazy<AssetSource>,
|
||||
private val trc20Source: Lazy<AssetSource>,
|
||||
private val unsupportedBalanceSource: AssetSource,
|
||||
|
||||
private val nativeAssetEventDetector: NativeAssetEventDetector,
|
||||
@@ -45,6 +47,8 @@ class TypeBasedAssetSourceRegistry(
|
||||
is Chain.Asset.Type.EvmErc20 -> evmErc20Source.get()
|
||||
is Chain.Asset.Type.EvmNative -> evmNativeSource.get()
|
||||
is Chain.Asset.Type.Equilibrium -> equilibriumAssetSource.get()
|
||||
is Chain.Asset.Type.TronNative -> tronNativeSource.get()
|
||||
is Chain.Asset.Type.Trc20 -> trc20Source.get()
|
||||
Chain.Asset.Type.Unsupported -> unsupportedBalanceSource
|
||||
}
|
||||
}
|
||||
@@ -57,6 +61,8 @@ class TypeBasedAssetSourceRegistry(
|
||||
add(evmNativeSource.get())
|
||||
add(evmErc20Source.get())
|
||||
add(equilibriumAssetSource.get())
|
||||
add(tronNativeSource.get())
|
||||
add(trc20Source.get())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +70,8 @@ class TypeBasedAssetSourceRegistry(
|
||||
return when (chainAsset.type) {
|
||||
is Chain.Asset.Type.Equilibrium,
|
||||
Chain.Asset.Type.EvmNative,
|
||||
is Chain.Asset.Type.TronNative,
|
||||
is Chain.Asset.Type.Trc20,
|
||||
|
||||
Chain.Asset.Type.Unsupported -> UnsupportedEventDetector()
|
||||
|
||||
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.trc20
|
||||
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.updateNonLockableAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.ChainAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdatePoint
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.tronNative.pollingBalanceFlow
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.TronGridApi
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.requireTronGridBaseUrl
|
||||
import io.novafoundation.nova.runtime.ext.requireTrc20
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* TRC-20 token balance on a Tron-based chain. Read-only (Phase 1): fetches via TronGrid's REST API (the same
|
||||
* `/v1/accounts/{address}` endpoint used for native TRX - TronGrid returns both in one response) and polls for
|
||||
* updates. No transfer/history support here - see `TronAssetsModule`.
|
||||
*/
|
||||
class Trc20AssetBalance(
|
||||
private val assetCache: AssetCache,
|
||||
private val tronGridApi: TronGridApi,
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
// TRC-20 tokens do not support locks
|
||||
return emptyFlow<Nothing>()
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
// TRC-20 tokens do not have an existential deposit concept
|
||||
return BigInteger.ZERO
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
val contractAddress = chainAsset.requireTrc20().contractAddress
|
||||
val address = chain.addressOf(accountId)
|
||||
|
||||
val balance = tronGridApi.fetchTrc20Balance(chain.requireTronGridBaseUrl(), address, contractAddress)
|
||||
|
||||
return ChainAssetBalance.fromFree(chainAsset, balance)
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
// Not on the critical sync path (mirrors EvmNativeAssetBalance) - out of scope for Phase 1 read-only support.
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
val contractAddress = chainAsset.requireTrc20().contractAddress
|
||||
val baseUrl = chain.requireTronGridBaseUrl()
|
||||
val address = chain.addressOf(accountId)
|
||||
|
||||
return pollingBalanceFlow { tronGridApi.fetchTrc20Balance(baseUrl, address, contractAddress) }
|
||||
.map { balance ->
|
||||
assetCache.updateNonLockableAsset(metaAccount.id, chainAsset, balance)
|
||||
|
||||
BalanceSyncUpdate.NoCause
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.tronNative
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
private const val TRON_BALANCE_POLLING_INTERVAL_MS = 30_000L
|
||||
|
||||
/**
|
||||
* TronGrid is a plain REST API with no push/subscription mechanism (unlike Ethereum nodes, which expose a
|
||||
* `newHeads`-style websocket subscription EVM balance sync piggybacks on). So balance updates for Tron-based
|
||||
* assets are polled instead of pushed: fetch immediately, then re-fetch on an interval, only emitting when the
|
||||
* balance actually changed.
|
||||
*/
|
||||
internal fun pollingBalanceFlow(
|
||||
intervalMs: Long = TRON_BALANCE_POLLING_INTERVAL_MS,
|
||||
fetch: suspend () -> Balance
|
||||
): Flow<Balance> = flow {
|
||||
var lastEmitted: Balance? = null
|
||||
|
||||
while (true) {
|
||||
val latest = fetch()
|
||||
|
||||
if (latest != lastEmitted) {
|
||||
lastEmitted = latest
|
||||
emit(latest)
|
||||
}
|
||||
|
||||
delay(intervalMs)
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets.balances.tronNative
|
||||
|
||||
import io.novafoundation.nova.core.updater.SharedRequestsBuilder
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.cache.updateNonLockableAsset
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.AssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.BalanceSyncUpdate
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.ChainAssetBalance
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.balances.model.TransferableBalanceUpdatePoint
|
||||
import io.novafoundation.nova.feature_wallet_impl.data.network.tron.TronGridApi
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.requireTronGridBaseUrl
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novasama.substrate_sdk_android.runtime.AccountId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.math.BigInteger
|
||||
|
||||
/**
|
||||
* Native TRX balance on a Tron-based chain. Read-only (Phase 1): fetches via TronGrid's REST API and polls for
|
||||
* updates, since TronGrid has no push/subscription mechanism. No transfer/history support here - see
|
||||
* `TronAssetsModule` for how this is paired with `UnsupportedAssetTransfers`/`UnsupportedAssetHistory`.
|
||||
*/
|
||||
class TronNativeAssetBalance(
|
||||
private val assetCache: AssetCache,
|
||||
private val tronGridApi: TronGridApi,
|
||||
) : AssetBalance {
|
||||
|
||||
override suspend fun startSyncingBalanceLocks(
|
||||
metaAccount: MetaAccount,
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<*> {
|
||||
// Tron native balance does not support locks
|
||||
return emptyFlow<Nothing>()
|
||||
}
|
||||
|
||||
override fun isSelfSufficient(chainAsset: Chain.Asset): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override suspend fun existentialDeposit(chainAsset: Chain.Asset): BigInteger {
|
||||
// Tron does not have an existential deposit concept
|
||||
return BigInteger.ZERO
|
||||
}
|
||||
|
||||
override suspend fun queryAccountBalance(chain: Chain, chainAsset: Chain.Asset, accountId: AccountId): ChainAssetBalance {
|
||||
val balance = tronGridApi.fetchNativeBalance(chain.requireTronGridBaseUrl(), chain.addressOf(accountId))
|
||||
|
||||
return ChainAssetBalance.fromFree(chainAsset, balance)
|
||||
}
|
||||
|
||||
override suspend fun subscribeAccountBalanceUpdatePoint(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
accountId: AccountId,
|
||||
): Flow<TransferableBalanceUpdatePoint> {
|
||||
// Not on the critical sync path (mirrors EvmNativeAssetBalance, which also leaves this unimplemented) -
|
||||
// out of scope for Phase 1 read-only support.
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun startSyncingBalance(
|
||||
chain: Chain,
|
||||
chainAsset: Chain.Asset,
|
||||
metaAccount: MetaAccount,
|
||||
accountId: AccountId,
|
||||
subscriptionBuilder: SharedRequestsBuilder
|
||||
): Flow<BalanceSyncUpdate> {
|
||||
val baseUrl = chain.requireTronGridBaseUrl()
|
||||
val address = chain.addressOf(accountId)
|
||||
|
||||
return pollingBalanceFlow { tronGridApi.fetchNativeBalance(baseUrl, address) }
|
||||
.map { balance ->
|
||||
assetCache.updateNonLockableAsset(metaAccount.id, chainAsset, balance)
|
||||
|
||||
BalanceSyncUpdate.NoCause
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -120,6 +120,7 @@ private fun Chain.Asset.existentialDepositError(amount: BigDecimal): WillRemoveA
|
||||
is Type.Orml -> WillRemoveAccount.WillBurnDust
|
||||
is Type.Statemine -> WillRemoveAccount.WillTransferDust(amount)
|
||||
is Type.EvmErc20, is Type.EvmNative -> WillRemoveAccount.WillBurnDust
|
||||
is Type.Trc20, Type.TronNative -> WillRemoveAccount.WillBurnDust
|
||||
is Type.Equilibrium -> WillRemoveAccount.WillBurnDust
|
||||
Type.Unsupported -> throw IllegalArgumentException("Unsupported")
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
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.TronAccountResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface RetrofitTronGridApi {
|
||||
|
||||
@GET
|
||||
@Headers(UserAgent.NOVA)
|
||||
suspend fun getAccount(@Url url: String): TronAccountResponse
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron
|
||||
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.types.Balance
|
||||
import java.math.BigInteger
|
||||
|
||||
interface TronGridApi {
|
||||
|
||||
suspend fun fetchNativeBalance(baseUrl: String, address: String): Balance
|
||||
|
||||
suspend fun fetchTrc20Balance(baseUrl: String, address: String, contractAddress: String): Balance
|
||||
}
|
||||
|
||||
class RealTronGridApi(
|
||||
private val retrofitApi: RetrofitTronGridApi
|
||||
) : TronGridApi {
|
||||
|
||||
override suspend fun fetchNativeBalance(baseUrl: String, address: String): Balance {
|
||||
val accountData = fetchAccountData(baseUrl, address) ?: return BigInteger.ZERO
|
||||
|
||||
return accountData.balance?.toBigInteger() ?: BigInteger.ZERO
|
||||
}
|
||||
|
||||
override suspend fun fetchTrc20Balance(baseUrl: String, address: String, contractAddress: String): Balance {
|
||||
val accountData = fetchAccountData(baseUrl, address) ?: return BigInteger.ZERO
|
||||
|
||||
val rawBalance = accountData.trc20.orEmpty()
|
||||
.firstNotNullOfOrNull { entry -> entry[contractAddress] }
|
||||
|
||||
return rawBalance?.toBigIntegerOrNull() ?: BigInteger.ZERO
|
||||
}
|
||||
|
||||
private suspend fun fetchAccountData(baseUrl: String, address: String) = retrofitApi.getAccount(
|
||||
url = accountUrl(baseUrl, address)
|
||||
).data?.firstOrNull()
|
||||
|
||||
private fun accountUrl(baseUrl: String, address: String): String {
|
||||
return "${baseUrl.trimEnd('/')}/v1/accounts/$address"
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.data.network.tron.model
|
||||
|
||||
/**
|
||||
* Response shape of TronGrid's `GET /v1/accounts/{address}`.
|
||||
*
|
||||
* An account that has never received any TRX/TRC20 transfer is not yet "activated" on-chain and TronGrid
|
||||
* returns an empty `data` array for it (not an error) - callers should treat that as a zero balance.
|
||||
*/
|
||||
class TronAccountResponse(
|
||||
val data: List<TronAccountData>? = null,
|
||||
val success: Boolean = true
|
||||
)
|
||||
|
||||
class TronAccountData(
|
||||
/**
|
||||
* Native TRX balance, denominated in SUN (1 TRX = 1_000_000 SUN), matching this chain's configured `precision: 6`.
|
||||
* Absent for freshly-activated accounts that hold TRX but have never been observed with a balance field by the indexer.
|
||||
*/
|
||||
val balance: Long? = null,
|
||||
|
||||
/**
|
||||
* List of single-entry maps: TRC20 contract address (Base58Check, same format as our chain config's `contractAddress`) -> balance string.
|
||||
* Only contains entries for tokens the account has ever interacted with; a token missing from this list means a zero balance.
|
||||
*/
|
||||
val trc20: List<Map<String, String>>? = null
|
||||
)
|
||||
+5
@@ -21,6 +21,7 @@ import io.novafoundation.nova.feature_wallet_impl.data.network.blockchain.assets
|
||||
EvmErc20AssetsModule::class,
|
||||
EvmNativeAssetsModule::class,
|
||||
EquilibriumAssetsModule::class,
|
||||
TronAssetsModule::class,
|
||||
UnsupportedAssetsModule::class
|
||||
]
|
||||
)
|
||||
@@ -35,6 +36,8 @@ class AssetsModule {
|
||||
@EvmErc20Assets evmErc20: Lazy<AssetSource>,
|
||||
@EvmNativeAssets evmNative: Lazy<AssetSource>,
|
||||
@EquilibriumAsset equilibrium: Lazy<AssetSource>,
|
||||
@TronNativeAssets tronNative: Lazy<AssetSource>,
|
||||
@Trc20Assets trc20: Lazy<AssetSource>,
|
||||
@UnsupportedAssets unsupported: AssetSource,
|
||||
|
||||
nativeAssetEventDetector: NativeAssetEventDetector,
|
||||
@@ -48,6 +51,8 @@ class AssetsModule {
|
||||
evmErc20Source = evmErc20,
|
||||
evmNativeSource = evmNative,
|
||||
equilibriumAssetSource = equilibrium,
|
||||
tronNativeSource = tronNative,
|
||||
trc20Source = trc20,
|
||||
unsupportedBalanceSource = unsupported,
|
||||
|
||||
nativeAssetEventDetector = nativeAssetEventDetector,
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package io.novafoundation.nova.feature_wallet_impl.di.modules
|
||||
|
||||
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_wallet_api.data.cache.AssetCache
|
||||
import io.novafoundation.nova.feature_wallet_api.data.network.blockhain.assets.AssetSource
|
||||
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.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 javax.inject.Qualifier
|
||||
|
||||
@Qualifier
|
||||
annotation class TronNativeAssets
|
||||
|
||||
@Qualifier
|
||||
annotation class Trc20Assets
|
||||
|
||||
/**
|
||||
* Tron/TRC-20 support - Phase 1, read-only.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
@Module
|
||||
class TronAssetsModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideRetrofitTronGridApi(
|
||||
networkApiCreator: NetworkApiCreator
|
||||
): RetrofitTronGridApi = networkApiCreator.create(RetrofitTronGridApi::class.java)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTronGridApi(retrofitTronGridApi: RetrofitTronGridApi): TronGridApi = RealTronGridApi(retrofitTronGridApi)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTronNativeBalance(assetCache: AssetCache, tronGridApi: TronGridApi) = TronNativeAssetBalance(assetCache, tronGridApi)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTrc20Balance(assetCache: AssetCache, tronGridApi: TronGridApi) = Trc20AssetBalance(assetCache, tronGridApi)
|
||||
|
||||
@Provides
|
||||
@TronNativeAssets
|
||||
@FeatureScope
|
||||
fun provideTronNativeAssetSource(
|
||||
tronNativeAssetBalance: TronNativeAssetBalance,
|
||||
unsupportedAssetTransfers: UnsupportedAssetTransfers,
|
||||
unsupportedAssetHistory: UnsupportedAssetHistory,
|
||||
): AssetSource = StaticAssetSource(
|
||||
transfers = unsupportedAssetTransfers,
|
||||
balance = tronNativeAssetBalance,
|
||||
history = unsupportedAssetHistory
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Trc20Assets
|
||||
@FeatureScope
|
||||
fun provideTrc20AssetSource(
|
||||
trc20AssetBalance: Trc20AssetBalance,
|
||||
unsupportedAssetTransfers: UnsupportedAssetTransfers,
|
||||
unsupportedAssetHistory: UnsupportedAssetHistory,
|
||||
): AssetSource = StaticAssetSource(
|
||||
transfers = unsupportedAssetTransfers,
|
||||
balance = trc20AssetBalance,
|
||||
history = unsupportedAssetHistory
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,11 @@ import io.novafoundation.nova.common.utils.emptySubstrateAccountId
|
||||
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.substrateAccountId
|
||||
import io.novafoundation.nova.common.utils.toTronAddress
|
||||
import io.novafoundation.nova.common.utils.tronAddressToAccountId
|
||||
import io.novafoundation.nova.common.utils.tronPublicKeyToAccountId
|
||||
import io.novafoundation.nova.core_db.model.AssetAndChainId
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain.Asset.StakingType.ALEPH_ZERO
|
||||
@@ -266,10 +270,10 @@ fun Chain.supportsLegacyAddressFormat() = legacyAddressPrefix != null
|
||||
fun Chain.requireGenesisHash() = requireNotNull(genesisHash)
|
||||
|
||||
fun Chain.addressOf(accountId: ByteArray): String {
|
||||
return if (isEthereumBased) {
|
||||
accountId.toEthereumAddress()
|
||||
} else {
|
||||
accountId.toAddress(addressPrefix.toShort())
|
||||
return when {
|
||||
isTronBased -> accountId.toTronAddress()
|
||||
isEthereumBased -> accountId.toEthereumAddress()
|
||||
else -> accountId.toAddress(addressPrefix.toShort())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +282,7 @@ fun Chain.addressOf(accountId: AccountIdKey): String {
|
||||
}
|
||||
|
||||
fun Chain.legacyAddressOfOrNull(accountId: ByteArray): String? {
|
||||
return if (isEthereumBased) {
|
||||
return if (isEthereumBased || isTronBased) {
|
||||
null
|
||||
} else {
|
||||
legacyAddressPrefix?.let { accountId.toAddress(it.toShort()) }
|
||||
@@ -290,10 +294,10 @@ fun ByteArray.toEthereumAddress(): String {
|
||||
}
|
||||
|
||||
fun Chain.accountIdOf(address: String): ByteArray {
|
||||
return if (isEthereumBased) {
|
||||
address.asEthereumAddress().toAccountId().value
|
||||
} else {
|
||||
address.toAccountId()
|
||||
return when {
|
||||
isTronBased -> address.tronAddressToAccountId()
|
||||
isEthereumBased -> address.asEthereumAddress().toAccountId().value
|
||||
else -> address.toAccountId()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,10 +327,10 @@ fun Chain.accountIdOrNull(address: String): ByteArray? {
|
||||
return runCatching { accountIdOf(address) }.getOrNull()
|
||||
}
|
||||
|
||||
fun Chain.emptyAccountId() = if (isEthereumBased) {
|
||||
emptyEthereumAccountId()
|
||||
} else {
|
||||
emptySubstrateAccountId()
|
||||
fun Chain.emptyAccountId() = when {
|
||||
isTronBased -> emptyTronAccountId()
|
||||
isEthereumBased -> emptyEthereumAccountId()
|
||||
else -> emptySubstrateAccountId()
|
||||
}
|
||||
|
||||
fun Chain.emptyAccountIdKey() = emptyAccountId().intoKey()
|
||||
@@ -336,10 +340,10 @@ fun Chain.accountIdOrDefault(maybeAddress: String): ByteArray {
|
||||
}
|
||||
|
||||
fun Chain.accountIdOf(publicKey: ByteArray): ByteArray {
|
||||
return if (isEthereumBased) {
|
||||
publicKey.asEthereumPublicKey().toAccountId().value
|
||||
} else {
|
||||
publicKey.substrateAccountId()
|
||||
return when {
|
||||
isTronBased -> publicKey.tronPublicKeyToAccountId()
|
||||
isEthereumBased -> publicKey.asEthereumPublicKey().toAccountId().value
|
||||
else -> publicKey.substrateAccountId()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,6 +529,26 @@ fun Chain.Asset.requireErc20(): Type.EvmErc20 {
|
||||
return type
|
||||
}
|
||||
|
||||
fun Chain.Asset.requireTrc20(): Type.Trc20 {
|
||||
require(type is Type.Trc20)
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
/**
|
||||
* The TronGrid-style REST API base url for a Tron-based chain.
|
||||
* Unlike EVM chains (where balance/rpc calls go through the chain's `nodes` JSON-RPC/WS endpoints and only tx-history
|
||||
* REST APIs are sourced from a separate `externalApis` entry), Tron has no JSON-RPC/WS node concept at all - the
|
||||
* configured `nodes` entry directly *is* the TronGrid REST API base url used for both balance and history.
|
||||
*/
|
||||
fun Chain.requireTronGridBaseUrl(): String {
|
||||
require(isTronBased) { "Chain $id is not Tron-based" }
|
||||
|
||||
return requireNotNull(nodes.nodes.minByOrNull { it.orderId }?.unformattedUrl) {
|
||||
"No TronGrid node configured for chain $id"
|
||||
}
|
||||
}
|
||||
|
||||
fun Chain.Asset.requireEquilibrium(): Type.Equilibrium {
|
||||
require(type is Type.Equilibrium)
|
||||
|
||||
@@ -588,6 +612,8 @@ val Chain.Asset.onChainAssetId: String?
|
||||
is Type.EvmErc20 -> this.type.contractAddress
|
||||
is Type.Native -> null
|
||||
is Type.EvmNative -> null
|
||||
is Type.Trc20 -> this.type.contractAddress
|
||||
Type.TronNative -> null
|
||||
Type.Unsupported -> error("Unsupported assetId type: ${this.type::class.simpleName}")
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -9,6 +9,9 @@ const val ASSET_UNSUPPORTED = "unsupported"
|
||||
const val ASSET_EVM_ERC20 = "evm"
|
||||
const val ASSET_EVM_NATIVE = "evmNative"
|
||||
|
||||
const val ASSET_TRON_NATIVE = "tronNative"
|
||||
const val ASSET_TRC20 = "trc20"
|
||||
|
||||
const val ASSET_EQUILIBRIUM = "equilibrium"
|
||||
const val ASSET_EQUILIBRIUM_ON_CHAIN_ID = "assetId"
|
||||
|
||||
|
||||
+7
@@ -59,6 +59,12 @@ fun mapChainAssetTypeToRaw(type: Chain.Asset.Type): Pair<String, Map<String, Any
|
||||
|
||||
is Chain.Asset.Type.EvmNative -> ASSET_EVM_NATIVE to null
|
||||
|
||||
is Chain.Asset.Type.TronNative -> ASSET_TRON_NATIVE to null
|
||||
|
||||
is Chain.Asset.Type.Trc20 -> ASSET_TRC20 to mapOf(
|
||||
EVM_EXTRAS_CONTRACT_ADDRESS to type.contractAddress
|
||||
)
|
||||
|
||||
is Chain.Asset.Type.Equilibrium -> ASSET_EQUILIBRIUM to mapOf(
|
||||
ASSET_EQUILIBRIUM_ON_CHAIN_ID to type.id.toString()
|
||||
)
|
||||
@@ -121,6 +127,7 @@ fun mapChainToLocal(chain: Chain, gson: Gson): ChainLocal {
|
||||
prefix = chain.addressPrefix,
|
||||
legacyPrefix = chain.legacyAddressPrefix,
|
||||
isEthereumBased = chain.isEthereumBased,
|
||||
isTronBased = chain.isTronBased,
|
||||
isTestNet = chain.isTestNet,
|
||||
hasSubstrateRuntime = chain.hasSubstrateRuntime,
|
||||
pushSupport = chain.pushSupport,
|
||||
|
||||
+9
@@ -85,6 +85,14 @@ private fun mapChainAssetTypeFromRaw(type: String?, typeExtras: Map<String, Any?
|
||||
|
||||
ASSET_EVM_NATIVE -> Chain.Asset.Type.EvmNative
|
||||
|
||||
ASSET_TRON_NATIVE -> Chain.Asset.Type.TronNative
|
||||
|
||||
ASSET_TRC20 -> {
|
||||
Chain.Asset.Type.Trc20(
|
||||
contractAddress = typeExtras!![EVM_EXTRAS_CONTRACT_ADDRESS] as String
|
||||
)
|
||||
}
|
||||
|
||||
ASSET_EQUILIBRIUM -> Chain.Asset.Type.Equilibrium((typeExtras!![ASSET_EQUILIBRIUM_ON_CHAIN_ID] as String).toBigInteger())
|
||||
|
||||
else -> Chain.Asset.Type.Unsupported
|
||||
@@ -267,6 +275,7 @@ fun mapChainLocalToChain(
|
||||
addressPrefix = prefix,
|
||||
legacyAddressPrefix = legacyPrefix,
|
||||
isEthereumBased = isEthereumBased,
|
||||
isTronBased = isTronBased,
|
||||
isTestNet = isTestNet,
|
||||
hasCrowdloans = hasCrowdloans,
|
||||
pushSupport = pushSupport,
|
||||
|
||||
+2
@@ -20,6 +20,7 @@ import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainAsset
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.remote.model.ChainRemote
|
||||
|
||||
private const val ETHEREUM_OPTION = "ethereumBased"
|
||||
private const val TRON_OPTION = "tronBased"
|
||||
private const val CROWDLOAN_OPTION = "crowdloans"
|
||||
private const val TESTNET_OPTION = "testnet"
|
||||
private const val PROXY_OPTION = "proxy"
|
||||
@@ -90,6 +91,7 @@ fun mapRemoteChainToLocal(
|
||||
prefix = addressPrefix,
|
||||
legacyPrefix = legacyAddressPrefix,
|
||||
isEthereumBased = ETHEREUM_OPTION in optionsOrEmpty,
|
||||
isTronBased = TRON_OPTION in optionsOrEmpty,
|
||||
isTestNet = TESTNET_OPTION in optionsOrEmpty,
|
||||
hasCrowdloans = CROWDLOAN_OPTION in optionsOrEmpty,
|
||||
supportProxy = PROXY_OPTION in optionsOrEmpty,
|
||||
|
||||
+14
@@ -32,6 +32,7 @@ data class Chain(
|
||||
val legacyAddressPrefix: Int?,
|
||||
val types: Types?,
|
||||
val isEthereumBased: Boolean,
|
||||
val isTronBased: Boolean,
|
||||
val isTestNet: Boolean,
|
||||
val source: Source,
|
||||
val hasSubstrateRuntime: Boolean,
|
||||
@@ -124,6 +125,19 @@ data class Chain(
|
||||
val id: BigInteger
|
||||
) : Type()
|
||||
|
||||
/**
|
||||
* Native TRX balance on a Tron-based chain.
|
||||
* Balance is fetched from a TronGrid-style REST API rather than JSON-RPC.
|
||||
*/
|
||||
object TronNative : Type()
|
||||
|
||||
/**
|
||||
* TRC-20 token on a Tron-based chain (EVM-compatible token standard, different REST API shape from Ethereum's ERC-20).
|
||||
*/
|
||||
data class Trc20(
|
||||
val contractAddress: String
|
||||
) : Type()
|
||||
|
||||
object Unsupported : Type()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user