diff --git a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt index 78260131..2c60d1e1 100644 --- a/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt +++ b/feature-settings-impl/src/main/java/io/novafoundation/nova/feature_settings_impl/domain/NetworkManagementChainInteractor.kt @@ -9,6 +9,7 @@ import io.novafoundation.nova.runtime.ext.isCustomNetwork import io.novafoundation.nova.runtime.ext.isDisabled import io.novafoundation.nova.runtime.ext.isEnabled import io.novafoundation.nova.runtime.ext.selectedUnformattedWssNodeUrlOrNull +import io.novafoundation.nova.runtime.ext.httpNodes import io.novafoundation.nova.runtime.ext.wssNodes import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain @@ -132,7 +133,13 @@ class RealNetworkManagementChainInteractor( } private fun nodesHealthState(chain: Chain, coroutineScope: CoroutineScope): Flow> { - return chain.nodes.wssNodes().map { + // wssNodes() alone leaves an HTTPS-only chain (Tron today - no wss endpoint at all) with an empty list + // here, which silently renders as "nothing to show" rather than a real health state - fall back to the + // http nodes only when there are no wss ones, since a chain that genuinely has wss nodes should still + // prefer testing those. + val nodesToCheck = chain.nodes.wssNodes().ifEmpty { chain.nodes.httpNodes() } + + return nodesToCheck.map { nodeHealthState(chain, it, coroutineScope) }.combine() } diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt index 97e74a26..5b347e11 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/di/ChainRegistryModule.kt @@ -37,7 +37,9 @@ import io.novafoundation.nova.runtime.multiNetwork.runtime.types.BaseTypeSynchro import io.novafoundation.nova.runtime.multiNetwork.runtime.types.TypesFetcher 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 @@ -165,7 +167,13 @@ class ChainRegistryModule { socketProvider, connectionSecrets, bulkRetriever, - web3ApiFactory + web3ApiFactory, + // 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 diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/node/healthState/NodeHealthStateTesterFactory.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/node/healthState/NodeHealthStateTesterFactory.kt index b9986a1b..7197def1 100644 --- a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/node/healthState/NodeHealthStateTesterFactory.kt +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/node/healthState/NodeHealthStateTesterFactory.kt @@ -5,6 +5,7 @@ import io.novafoundation.nova.runtime.ethereum.Web3ApiFactory import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain import io.novafoundation.nova.runtime.multiNetwork.connection.ConnectionSecrets import io.novasama.substrate_sdk_android.wsrpc.SocketService +import okhttp3.OkHttpClient import javax.inject.Provider import kotlinx.coroutines.CoroutineScope @@ -12,15 +13,21 @@ class NodeHealthStateTesterFactory( private val socketServiceProvider: Provider, private val connectionSecrets: ConnectionSecrets, private val bulkRetriever: BulkRetriever, - private val web3ApiFactory: Web3ApiFactory + private val web3ApiFactory: Web3ApiFactory, + private val httpClient: OkHttpClient, ) { fun create(chain: Chain, node: Chain.Node, coroutineScope: CoroutineScope): NodeHealthStateTester { val nodeIsSupported = chain.nodes.nodes.any { it.unformattedUrl == node.unformattedUrl } require(nodeIsSupported) - return if (chain.hasSubstrateRuntime) { - SubstrateNodeHealthStateTester( + return when { + chain.isTronBased -> TronNodeHealthStateTester( + node = node, + httpClient = httpClient + ) + + chain.hasSubstrateRuntime -> SubstrateNodeHealthStateTester( chain = chain, socketService = socketServiceProvider.get(), connectionSecrets = connectionSecrets, @@ -28,8 +35,8 @@ class NodeHealthStateTesterFactory( node = node, coroutineScope = coroutineScope ) - } else { - EthereumNodeHealthStateTester( + + else -> EthereumNodeHealthStateTester( socketService = socketServiceProvider.get(), connectionSecrets = connectionSecrets, node = node, diff --git a/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/node/healthState/TronNodeHealthStateTester.kt b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/node/healthState/TronNodeHealthStateTester.kt new file mode 100644 index 00000000..c19576ac --- /dev/null +++ b/runtime/src/main/java/io/novafoundation/nova/runtime/multiNetwork/connection/node/healthState/TronNodeHealthStateTester.kt @@ -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 = 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 + } + } +}