mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-07-22 18:25:50 +00:00
merge: reconcile with feature/trc20-tron-send into one combined branch
Bitcoin was branched off main independently instead of continuing on top of the Tron send branch, which caused real divergence: both branches touch the same shared signing/DI code (multiChainEncryptionFor, getMetaAccountKeypair, NodeHealthStateTesterFactory, AssetsModule/TypeBasedAssetSourceRegistry, CloudBackup schema, Fee model), and both need to ship together in one coordinated release once wallet-util's master is unblocked. Merging now reconciles that before it gets any harder, and gives one branch/versionCode history for device testing instead of two that silently diverge. Conflict resolutions of note: - SecretStoreV2/SecretsSigner: getKeypair and multiChainEncryptionFor now recognize Tron AND Bitcoin accounts (isTronBased/isBitcoinBased both threaded through, checked before the Ethereum fallback). - AccountDataSourceImpl: both address-backfill migrations now run sequentially in one coroutine (Tron's fix for a race against the legacy migration applies equally to Bitcoin's backfill). - CloudBackup: kept Tron's forward-looking Solana schema reservation alongside Tron's and Bitcoin's now-real fields. - ChainRegistryModule/NodeHealthStateTesterFactory: adopted Tron's self-contained short-lived OkHttpClient (simpler than threading the shared app-wide client through RuntimeDependencies, which is reverted). - RealSecretsMetaAccount: extended Tron's chain-account MultiChainEncryption fix to also cover Bitcoin, for the same underlying reason. - ChainExt.isValidAddress: Tron's branch independently closed the pre-existing Tron validation gap; kept alongside Bitcoin's own check.
This commit is contained in:
@@ -16,10 +16,12 @@ android {
|
||||
buildConfigField "String", "PRE_CONFIGURED_CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/pending/post-fix-release/chains/v22/preConfigured/chains.json\""
|
||||
buildConfigField "String", "PRE_CONFIGURED_CHAIN_DETAILS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/pending/post-fix-release/chains/v22/preConfigured/details\""
|
||||
|
||||
buildConfigField "String", "TEST_CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/tests/chains_for_testBalance.json\""
|
||||
buildConfigField "String", "TEST_CHAINS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/pending/post-fix-release/tests/chains_for_testBalance.json\""
|
||||
buildConfigField "String", "TEST_ASSETS_URL", "\"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/pending/post-fix-release/tests/pezkuwi_assets_for_testBalance.json\""
|
||||
|
||||
buildConfigField "String", "INFURA_API_KEY", readStringSecret("INFURA_API_KEY")
|
||||
buildConfigField "String", "DWELLIR_API_KEY", readStringSecret("DWELLIR_API_KEY")
|
||||
buildConfigField "String", "TRONGRID_API_KEY", readStringSecret("TRONGRID_API_KEY")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -39,6 +39,7 @@ import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.web3j.protocol.http.HttpService
|
||||
import javax.inject.Provider
|
||||
|
||||
@@ -162,13 +163,17 @@ class ChainRegistryModule {
|
||||
bulkRetriever: BulkRetriever,
|
||||
connectionSecrets: ConnectionSecrets,
|
||||
web3ApiFactory: Web3ApiFactory,
|
||||
httpClient: OkHttpClient,
|
||||
) = NodeHealthStateTesterFactory(
|
||||
socketProvider,
|
||||
connectionSecrets,
|
||||
bulkRetriever,
|
||||
web3ApiFactory,
|
||||
httpClient
|
||||
// A short-lived, minimally-configured client is enough for a health-check ping - unlike Web3ApiFactory's
|
||||
// client, this never needs to survive/reuse connections across a long-lived RPC session.
|
||||
OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.build()
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -10,14 +10,11 @@ import io.novafoundation.nova.core_db.dao.ChainAssetDao
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.core_db.dao.StorageDao
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
interface RuntimeDependencies {
|
||||
|
||||
fun networkApiCreator(): NetworkApiCreator
|
||||
|
||||
fun okHttpClient(): OkHttpClient
|
||||
|
||||
fun socketServiceCreator(): SocketService
|
||||
|
||||
fun gson(): Gson
|
||||
|
||||
@@ -19,6 +19,7 @@ import io.novafoundation.nova.common.utils.formatNamed
|
||||
import io.novafoundation.nova.common.utils.isValidBitcoinAddress
|
||||
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.toBitcoinAddress
|
||||
import io.novafoundation.nova.common.utils.toTronAddress
|
||||
@@ -372,7 +373,13 @@ fun Chain.isValidAddress(address: String): Boolean {
|
||||
return runCatching {
|
||||
when {
|
||||
isBitcoinBased -> address.isValidBitcoinAddress()
|
||||
|
||||
// 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()
|
||||
|
||||
isEthereumBased -> address.asEthereumAddress().isValid()
|
||||
|
||||
else -> {
|
||||
address.toAccountId() // verify supplied address can be converted to account id
|
||||
|
||||
@@ -492,6 +499,7 @@ object ChainGeneses {
|
||||
object ChainIds {
|
||||
|
||||
const val ETHEREUM = "$EIP_155_PREFIX:1"
|
||||
const val TRON = "tron:0x2b6653dc"
|
||||
|
||||
const val MOONBEAM = ChainGeneses.MOONBEAM
|
||||
const val MOONRIVER = ChainGeneses.MOONRIVER
|
||||
@@ -503,6 +511,33 @@ val Chain.Companion.Geneses
|
||||
val Chain.Companion.Ids
|
||||
get() = ChainIds
|
||||
|
||||
/**
|
||||
* A short, user-facing token-standard label for chains where disambiguating "which token standard is this"
|
||||
* is actually useful (multiple ecosystems all issue their own USDT/USDC etc., so a bare chain name isn't
|
||||
* always enough context). Deliberately NOT derived from [Chain.Asset.Type] (e.g. every Statemine-type chain
|
||||
* would get the same label) - this is chain-specific by design, matching exactly which labels are
|
||||
* recognizable/expected by users (PEZ-20, ERC-20, TRC-20), not a mechanical one-label-per-asset-type mapping.
|
||||
*/
|
||||
val Chain.assetStandardLabelOrNull: String?
|
||||
get() = when {
|
||||
genesisHash == Chain.Geneses.PEZKUWI_ASSET_HUB -> "PEZ-20"
|
||||
id == Chain.Ids.ETHEREUM -> "ERC-20"
|
||||
id == Chain.Ids.TRON -> "TRC-20"
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain display name with its token-standard label appended where [assetStandardLabelOrNull] applies, e.g.
|
||||
* "Ethereum (ERC-20)". Shared across every screen that lists the same token symbol once per chain (the
|
||||
* Send/Receive/etc. network picker, the main balance list's per-token chain breakdown) - a bare chain name
|
||||
* alone doesn't convey which issuance this is when multiple ecosystems share the same symbol.
|
||||
*/
|
||||
fun Chain.displayNameWithAssetStandard(): String {
|
||||
val standardLabel = assetStandardLabelOrNull ?: return name
|
||||
|
||||
return "$name ($standardLabel)"
|
||||
}
|
||||
|
||||
fun Chain.Asset.requireStatemine(): Type.Statemine {
|
||||
require(type is Type.Statemine)
|
||||
|
||||
|
||||
@@ -3,19 +3,23 @@ package io.novafoundation.nova.runtime.ext
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
|
||||
val Chain.mainChainsFirstAscendingOrder
|
||||
get() = when (genesisHash) {
|
||||
get() = when {
|
||||
// Pezkuwi ecosystem first
|
||||
Chain.Geneses.PEZKUWI -> 0
|
||||
Chain.Geneses.PEZKUWI_ASSET_HUB -> 1
|
||||
Chain.Geneses.PEZKUWI_PEOPLE -> 2
|
||||
genesisHash == Chain.Geneses.PEZKUWI -> 0
|
||||
genesisHash == Chain.Geneses.PEZKUWI_ASSET_HUB -> 1
|
||||
genesisHash == Chain.Geneses.PEZKUWI_PEOPLE -> 2
|
||||
// Then Polkadot ecosystem
|
||||
Chain.Geneses.POLKADOT -> 3
|
||||
Chain.Geneses.POLKADOT_ASSET_HUB -> 4
|
||||
genesisHash == Chain.Geneses.POLKADOT -> 3
|
||||
genesisHash == Chain.Geneses.POLKADOT_ASSET_HUB -> 4
|
||||
// Then Kusama ecosystem
|
||||
Chain.Geneses.KUSAMA -> 5
|
||||
Chain.Geneses.KUSAMA_ASSET_HUB -> 6
|
||||
genesisHash == Chain.Geneses.KUSAMA -> 5
|
||||
genesisHash == Chain.Geneses.KUSAMA_ASSET_HUB -> 6
|
||||
// Then Ethereum, then Tron - not identified by genesisHash (that's substrate-only), so this can't
|
||||
// stay a `when (genesisHash)` subject match once these two are added
|
||||
id == Chain.Ids.ETHEREUM -> 7
|
||||
id == Chain.Ids.TRON -> 8
|
||||
// Everything else
|
||||
else -> 7
|
||||
else -> 9
|
||||
}
|
||||
|
||||
val Chain.testnetsLastAscendingOrder
|
||||
|
||||
@@ -10,7 +10,14 @@ val TokenSymbol.mainTokensFirstAscendingOrder
|
||||
"DOT" -> 3
|
||||
"KSM" -> 4
|
||||
"USDC" -> 5
|
||||
else -> 6
|
||||
"TRX" -> 6
|
||||
"BTC" -> 7
|
||||
"ETH" -> 8
|
||||
"BNB" -> 9
|
||||
"AVAX" -> 10
|
||||
"LINK" -> 11
|
||||
"TAO" -> 12
|
||||
else -> 13
|
||||
}
|
||||
|
||||
val TokenSymbol.alphabeticalOrder
|
||||
|
||||
@@ -7,7 +7,7 @@ import io.novafoundation.nova.common.utils.RuntimeContext
|
||||
import io.novafoundation.nova.common.utils.diffed
|
||||
import io.novafoundation.nova.common.utils.filterList
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.common.utils.mapListNotNull
|
||||
import io.novafoundation.nova.common.utils.mapNotNullToSet
|
||||
import io.novafoundation.nova.common.utils.provideContext
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
@@ -41,6 +41,7 @@ import io.novafoundation.nova.runtime.multiNetwork.runtime.types.BaseTypeSynchro
|
||||
import io.novasama.substrate_sdk_android.wsrpc.SocketService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -67,14 +68,37 @@ class ChainRegistry(
|
||||
private val runtimeSyncService: RuntimeSyncService,
|
||||
private val web3ApiPool: Web3ApiPool,
|
||||
private val gson: Gson
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
// SupervisorJob, not the plain Job a bare CoroutineScope(Dispatchers.Default) would give: without it, an
|
||||
// uncaught exception in ANY coroutine sharing this scope (e.g. currentChains'/chainsById's shareIn, or any
|
||||
// launch{} below) cancels every sibling, including the other one - a single malformed/leftover chain row
|
||||
// would then permanently kill sync for every chain, not just the offending one.
|
||||
) : CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.Default) {
|
||||
|
||||
val currentChains = chainDao.joinChainInfoFlow()
|
||||
.mapList { mapChainLocalToChain(it, gson) }
|
||||
// mapListNotNull, not mapList: mapChainLocalToChain() can throw on a single malformed row (e.g. a
|
||||
// gson.fromJson() failure on the chain's `additional` JSON blob) - since this whole step runs as ONE
|
||||
// transform over the ENTIRE chain list, one bad chain would previously throw out of this operator and
|
||||
// permanently kill this Eagerly-shared flow for every chain, not just the offending one. Skip and log
|
||||
// instead, matching the per-chain isolation already applied to registerChain/unregisterChain below.
|
||||
.mapListNotNull { chainLocal ->
|
||||
runCatching { mapChainLocalToChain(chainLocal, gson) }
|
||||
.onFailure { Log.e(LOG_TAG, "Failed to map chain ${chainLocal.chain.id} (${chainLocal.chain.name}) from local DB", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
.diffed()
|
||||
.map { diff ->
|
||||
diff.removed.forEach { unregisterChain(it) }
|
||||
diff.newOrUpdated.forEach { chain -> registerChain(chain) }
|
||||
// Each chain's register/unregister is isolated: one malformed/leftover row (e.g. a chain persisted
|
||||
// as disabled from an earlier session) must not throw out of this operator and kill this flow for
|
||||
// every other chain - shareIn(..., Eagerly) never restarts once its upstream completes/throws, so
|
||||
// any single unhandled exception here would silently and permanently break sync for the whole app.
|
||||
diff.removed.forEach { chain ->
|
||||
runCatching { unregisterChain(chain) }
|
||||
.onFailure { Log.e(LOG_TAG, "Failed to unregister chain ${chain.name} (${chain.id})", it) }
|
||||
}
|
||||
diff.newOrUpdated.forEach { chain ->
|
||||
runCatching { registerChain(chain) }
|
||||
.onFailure { Log.e(LOG_TAG, "Failed to register chain ${chain.name} (${chain.id})", it) }
|
||||
}
|
||||
|
||||
diff.all
|
||||
}
|
||||
|
||||
+14
@@ -1,7 +1,9 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.asset
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.retryUntilDone
|
||||
import io.novafoundation.nova.core_db.dao.ChainAssetDao
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
@@ -40,6 +42,18 @@ class EvmAssetsSyncService(
|
||||
new.copy(enabled = old?.enabled ?: ENABLED_DEFAULT_BOOL)
|
||||
}
|
||||
|
||||
// Same defensive guard as ChainSyncService: a transient upstream issue can make the fetch return
|
||||
// successfully with a suspiciously small/empty list. Diffing that against a populated local DB would
|
||||
// delete most or all of the user's ERC20 tokens (e.g. USDT-ERC20) - skip instead of wiping good data.
|
||||
if (oldAssets.isNotEmpty() && newAssets.size < oldAssets.size / 2) {
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"Refusing to apply EVM asset sync: remote returned ${newAssets.size} assets vs ${oldAssets.size} currently stored " +
|
||||
"(would remove more than half). Likely a transient fetch issue - skipping this sync cycle."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val diff = CollectionDiffer.findDiff(newAssets, oldAssets, forceUseNewItems = false)
|
||||
chainAssetDao.updateAssets(diff)
|
||||
}
|
||||
|
||||
+42
-9
@@ -1,7 +1,9 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.chain
|
||||
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.retryUntilDone
|
||||
import io.novafoundation.nova.core_db.dao.ChainDao
|
||||
import io.novafoundation.nova.core_db.dao.FullAssetIdLocal
|
||||
@@ -40,17 +42,48 @@ class ChainSyncService(
|
||||
|
||||
val remoteChains = retryUntilDone { chainFetcher.getChains() }
|
||||
|
||||
val newChains = remoteChains.map { mapRemoteChainToLocal(it, oldChainsById[it.chainId], source = ChainLocal.Source.DEFAULT, gson) }
|
||||
val newAssets = remoteChains.flatMap { chain ->
|
||||
chain.assets.map {
|
||||
val fullAssetId = FullAssetIdLocal(chain.chainId, it.assetId)
|
||||
val oldAsset = associatedOldAssets[fullAssetId]
|
||||
mapRemoteAssetToLocal(chain, it, gson, oldAsset?.enabled ?: ENABLED_DEFAULT_BOOL)
|
||||
// A transient upstream issue (CDN hiccup, regional network filtering, a bad publish) can make
|
||||
// chainFetcher.getChains() return successfully with a suspiciously small/empty list instead of
|
||||
// throwing. Applying that as a diff against a populated local DB would delete most or all of the
|
||||
// user's chains/assets - not a sync failure, but active data loss, for something that self-heals on
|
||||
// the next successful sync if we just skip applying it. Only guard when we HAD data: an empty result
|
||||
// on a genuinely first-ever sync is normal and must proceed.
|
||||
if (oldChains.isNotEmpty() && remoteChains.size < oldChains.size / 2) {
|
||||
Log.e(
|
||||
LOG_TAG,
|
||||
"Refusing to apply chain sync: remote returned ${remoteChains.size} chains vs ${oldChains.size} currently stored " +
|
||||
"(would remove more than half). Likely a transient fetch issue - skipping this sync cycle."
|
||||
)
|
||||
return@withContext
|
||||
}
|
||||
|
||||
// One malformed/incompatible chain (a new field the app's mapper doesn't understand yet, a bad
|
||||
// publish, etc.) must not take down sync for every other chain - a plain .map{} here means a single
|
||||
// throwing chain aborts before chainDao.applyDiff() is ever called, leaving a brand new install with
|
||||
// zero locally-cached chains forever (a completely empty tokens list), since nothing else in this
|
||||
// function ever gets a chance to run. Isolate failures per chain, and per asset within a chain that
|
||||
// otherwise mapped fine, instead.
|
||||
val remoteChainsWithLocal = remoteChains.mapNotNull { chainRemote ->
|
||||
runCatching { chainRemote to mapRemoteChainToLocal(chainRemote, oldChainsById[chainRemote.chainId], source = ChainLocal.Source.DEFAULT, gson) }
|
||||
.onFailure { Log.e(LOG_TAG, "Failed to map remote chain ${chainRemote.chainId} (${chainRemote.name}), skipping it for this sync cycle", it) }
|
||||
.getOrNull()
|
||||
}
|
||||
|
||||
val newChains = remoteChainsWithLocal.map { (_, chainLocal) -> chainLocal }
|
||||
val newAssets = remoteChainsWithLocal.flatMap { (chain, _) ->
|
||||
chain.assets.mapNotNull { assetRemote ->
|
||||
runCatching {
|
||||
val fullAssetId = FullAssetIdLocal(chain.chainId, assetRemote.assetId)
|
||||
val oldAsset = associatedOldAssets[fullAssetId]
|
||||
mapRemoteAssetToLocal(chain, assetRemote, gson, oldAsset?.enabled ?: ENABLED_DEFAULT_BOOL)
|
||||
}.onFailure {
|
||||
Log.e(LOG_TAG, "Failed to map asset ${assetRemote.assetId} (${assetRemote.symbol}) on chain ${chain.chainId}, skipping it", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
val newNodes = remoteChains.flatMap(::mapRemoteNodesToLocal)
|
||||
val newExplorers = remoteChains.flatMap(::mapRemoteExplorersToLocal)
|
||||
val newExternalApis = remoteChains.flatMap(::mapExternalApisToLocal)
|
||||
val newNodes = remoteChainsWithLocal.flatMap { (chain, _) -> mapRemoteNodesToLocal(chain) }
|
||||
val newExplorers = remoteChainsWithLocal.flatMap { (chain, _) -> mapRemoteExplorersToLocal(chain) }
|
||||
val newExternalApis = remoteChainsWithLocal.flatMap { (chain, _) -> mapExternalApisToLocal(chain) }
|
||||
val newNodeSelectionPreferences = nodeSelectionPreferencesFor(newChains, oldNodeSelectionPreferences)
|
||||
|
||||
val chainsDiff = CollectionDiffer.findDiff(newChains, oldChains, forceUseNewItems = false)
|
||||
|
||||
+5
@@ -27,6 +27,11 @@ class NodeHealthStateTesterFactory(
|
||||
httpClient = httpClient
|
||||
)
|
||||
|
||||
chain.isTronBased -> TronNodeHealthStateTester(
|
||||
node = node,
|
||||
httpClient = httpClient
|
||||
)
|
||||
|
||||
chain.hasSubstrateRuntime -> SubstrateNodeHealthStateTester(
|
||||
chain = chain,
|
||||
socketService = socketServiceProvider.get(),
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.runtime.multiNetwork.connection.node.healthState
|
||||
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTime
|
||||
|
||||
/**
|
||||
* TronGrid speaks a plain REST API, not Ethereum JSON-RPC - reusing [EthereumNodeHealthStateTester] against it
|
||||
* (as this codebase used to, before Tron nodes were included in health checks at all) would send an
|
||||
* `eth_getBalance` call TronGrid doesn't understand, always reporting the node as unreachable regardless of its
|
||||
* actual health. `GET /wallet/getchainparameters` needs no account/address context and is cheap on TronGrid's
|
||||
* side, making it a good generic liveness ping - same endpoint this codebase already uses elsewhere
|
||||
* (`TronGridApi.getChainParameters`), just called directly here since `runtime` cannot depend on
|
||||
* `feature-wallet-impl` (wrong direction) to reuse that Retrofit interface.
|
||||
*/
|
||||
class TronNodeHealthStateTester(
|
||||
private val node: Chain.Node,
|
||||
private val httpClient: OkHttpClient,
|
||||
) : NodeHealthStateTester {
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
override suspend fun testNodeHealthState(): Result<Long> = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val request = Request.Builder()
|
||||
.url("${node.unformattedUrl.trimEnd('/')}/wallet/getchainparameters")
|
||||
.build()
|
||||
|
||||
val duration = measureTime {
|
||||
httpClient.newCall(request).execute().use { response ->
|
||||
check(response.isSuccessful) { "HTTP ${response.code}" }
|
||||
}
|
||||
}
|
||||
|
||||
duration.inWholeMilliseconds
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
-22
@@ -14,7 +14,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlin.coroutines.coroutineContext
|
||||
@@ -24,35 +26,51 @@ abstract class ChainUpdaterGroupUpdateSystem(
|
||||
private val storageSharedRequestsBuilderFactory: StorageSharedRequestsBuilderFactory,
|
||||
) : UpdateSystem {
|
||||
|
||||
// Callers (MultiChainUpdateSystem, SingleChainUpdateSystem) merge several chains' runUpdaters() results
|
||||
// into one flow. chainRegistry.getRuntime(chain.id) throws for a chain whose runtime isn't ready yet
|
||||
// (including a disabled chain, via DisabledChainException) - if that throw escapes this function
|
||||
// uncaught, it propagates through the merge and kills governance/staking/crowdloan sync for every OTHER
|
||||
// chain in the group too, not just the failing one. Wrapping the whole body in flow{} + catch isolates
|
||||
// that failure to this chain alone.
|
||||
protected suspend fun runUpdaters(chain: Chain, updaters: Collection<Updater<*>>): Flow<Updater.SideEffect> {
|
||||
val runtimeMetadata = chainRegistry.getRuntime(chain.id).metadata
|
||||
return flow {
|
||||
val runtimeMetadata = chainRegistry.getRuntime(chain.id).metadata
|
||||
|
||||
val logTag = this@ChainUpdaterGroupUpdateSystem.LOG_TAG
|
||||
val selfName = this@ChainUpdaterGroupUpdateSystem::class.java.simpleName
|
||||
val logTag = this@ChainUpdaterGroupUpdateSystem.LOG_TAG
|
||||
val selfName = this@ChainUpdaterGroupUpdateSystem::class.java.simpleName
|
||||
|
||||
val scopeFlows = updaters.groupBy(Updater<*>::scope).map { (scope, scopeUpdaters) ->
|
||||
scope.invalidationFlow().flatMapLatest { scopeValue ->
|
||||
val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id)
|
||||
val scopeFlows = updaters.groupBy(Updater<*>::scope).map { (scope, scopeUpdaters) ->
|
||||
scope.invalidationFlow().flatMapLatest { scopeValue ->
|
||||
val subscriptionBuilder = storageSharedRequestsBuilderFactory.create(chain.id)
|
||||
|
||||
val updatersFlow = scopeUpdaters
|
||||
.filter { it.requiredModules.all(runtimeMetadata::hasModule) }
|
||||
.map { updater ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(updater as Updater<Any?>).listenForUpdates(subscriptionBuilder, scopeValue)
|
||||
.catch { Log.e(logTag, "Failed to start ${updater.javaClass.simpleName} in $selfName for ${chain.name}", it) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
val updatersFlow = scopeUpdaters
|
||||
.filter { it.requiredModules.all(runtimeMetadata::hasModule) }
|
||||
.map { updater ->
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(updater as Updater<Any?>).listenForUpdates(subscriptionBuilder, scopeValue)
|
||||
.catch { Log.e(logTag, "Failed to start ${updater.javaClass.simpleName} in $selfName for ${chain.name}", it) }
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
|
||||
if (updatersFlow.isNotEmpty()) {
|
||||
subscriptionBuilder.subscribe(coroutineContext)
|
||||
|
||||
updatersFlow.merge()
|
||||
} else {
|
||||
emptyFlow()
|
||||
}
|
||||
|
||||
if (updatersFlow.isNotEmpty()) {
|
||||
subscriptionBuilder.subscribe(coroutineContext)
|
||||
|
||||
updatersFlow.merge()
|
||||
} else {
|
||||
emptyFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return scopeFlows.merge()
|
||||
emitAll(scopeFlows.merge())
|
||||
}.catch { error ->
|
||||
// Explicitly qualified: unqualified LOG_TAG here would resolve against the nearest implicit
|
||||
// receiver, which is this catch lambda's FlowCollector, not this class - Any.LOG_TAG applies to
|
||||
// any receiver, so it would silently compile but log the wrong (unhelpful) tag.
|
||||
val outerLogTag = this@ChainUpdaterGroupUpdateSystem.LOG_TAG
|
||||
val outerSelfName = this@ChainUpdaterGroupUpdateSystem::class.java.simpleName
|
||||
|
||||
Log.e(outerLogTag, "Failed to start updaters in $outerSelfName for ${chain.name}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user